Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
232e7582b0
commit
57f6fa3cd7
|
@ -1 +1 @@
|
|||
c45afa70f5bd9723f0836ff228a11bc896c45511
|
||||
771df64aaf511cc3c64d7b55aee2d961941bfdab
|
||||
|
|
|
@ -84,6 +84,11 @@ export default {
|
|||
cancelActionProps() {
|
||||
return {
|
||||
text: this.$options.translations.cancelActionLabel,
|
||||
attributes: [
|
||||
{
|
||||
category: 'secondary',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
canRegenerateInstanceId() {
|
||||
|
@ -120,11 +125,11 @@ export default {
|
|||
<template>
|
||||
<gl-modal
|
||||
:modal-id="modalId"
|
||||
:action-cancel="cancelActionProps"
|
||||
:action-primary="regenerateInstanceIdActionProps"
|
||||
@canceled="clearState"
|
||||
:action-primary="cancelActionProps"
|
||||
:action-secondary="regenerateInstanceIdActionProps"
|
||||
@secondary.prevent="rotateToken"
|
||||
@hide="clearState"
|
||||
@primary.prevent="rotateToken"
|
||||
@primary="clearState"
|
||||
>
|
||||
<template #modal-title>
|
||||
{{ $options.translations.modalTitle }}
|
||||
|
|
|
@ -266,6 +266,7 @@ class GfmAutoComplete {
|
|||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
insertTpl: '${atwho-at}${username}',
|
||||
limit: 10,
|
||||
searchKey: 'search',
|
||||
alwaysHighlightFirst: true,
|
||||
skipSpecialCharacterTest: true,
|
||||
|
@ -311,6 +312,38 @@ class GfmAutoComplete {
|
|||
|
||||
return data;
|
||||
},
|
||||
sorter(query, items) {
|
||||
if (!query) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Disable auto-selecting the loading icon
|
||||
this.setting.highlightFirst = this.setting.alwaysHighlightFirst;
|
||||
if (GfmAutoComplete.isLoading(items)) {
|
||||
this.setting.highlightFirst = false;
|
||||
return items;
|
||||
}
|
||||
|
||||
const lowercaseQuery = query.toLowerCase();
|
||||
const members = items.slice();
|
||||
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
|
||||
|
||||
return members.sort((a, b) => {
|
||||
if (nameOrUsernameStartsWith(a, lowercaseQuery)) {
|
||||
return -1;
|
||||
}
|
||||
if (nameOrUsernameStartsWith(b, lowercaseQuery)) {
|
||||
return 1;
|
||||
}
|
||||
if (nameOrUsernameIncludes(a, lowercaseQuery)) {
|
||||
return -1;
|
||||
}
|
||||
if (nameOrUsernameIncludes(b, lowercaseQuery)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -772,6 +805,14 @@ GfmAutoComplete.Members = {
|
|||
title,
|
||||
)}${availabilityStatus}</small> ${icon}</li>`;
|
||||
},
|
||||
nameOrUsernameStartsWith(member, query) {
|
||||
// `member.search` is a name:username string like `MargeSimpson msimpson`
|
||||
return member.search.split(' ').some((name) => name.toLowerCase().startsWith(query));
|
||||
},
|
||||
nameOrUsernameIncludes(member, query) {
|
||||
// `member.search` is a name:username string like `MargeSimpson msimpson`
|
||||
return member.search.toLowerCase().includes(query);
|
||||
},
|
||||
};
|
||||
GfmAutoComplete.Labels = {
|
||||
templateFunction(color, title) {
|
||||
|
|
|
@ -122,10 +122,8 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
|
|||
});
|
||||
groupManager.startImport({ group, importId: response.data.id });
|
||||
} catch (e) {
|
||||
createFlash({
|
||||
message: s__('BulkImport|Importing the group failed'),
|
||||
});
|
||||
|
||||
const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed');
|
||||
createFlash({ message });
|
||||
groupManager.setImportStatus(group, STATUSES.NONE);
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import UserList from '~/user_lists/components/user_list.vue';
|
||||
import createStore from '~/user_lists/store/show';
|
||||
import featureFlagsUserListInit from '~/projects/feature_flags_user_lists/show/index';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const el = document.getElementById('js-edit-user-list');
|
||||
return new Vue({
|
||||
el,
|
||||
store: createStore(el.dataset),
|
||||
render(h) {
|
||||
const { emptyStatePath } = el.dataset;
|
||||
return h(UserList, { props: { emptyStatePath } });
|
||||
},
|
||||
});
|
||||
});
|
||||
featureFlagsUserListInit();
|
||||
|
|
|
@ -12,11 +12,9 @@
|
|||
* 4. Commit widget
|
||||
*/
|
||||
import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { PIPELINES_TABLE } from '../../constants';
|
||||
import eventHub from '../../event_hub';
|
||||
import JobItem from '../graph/job_item.vue';
|
||||
|
@ -31,19 +29,16 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
updateDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -57,11 +52,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isCiMiniPipelineGlDropdown() {
|
||||
// Feature flag ci_mini_pipeline_gl_dropdown
|
||||
// See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400
|
||||
return this.glFeatures?.ciMiniPipelineGlDropdown;
|
||||
},
|
||||
triggerButtonClass() {
|
||||
return `ci-status-icon-${this.stage.status.group}`;
|
||||
},
|
||||
|
@ -76,24 +66,12 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) {
|
||||
this.stopDropdownClickPropagation();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onShowDropdown() {
|
||||
eventHub.$emit('clickedDropdown');
|
||||
this.isLoading = true;
|
||||
this.fetchJobs();
|
||||
},
|
||||
onClickStage() {
|
||||
if (!this.isDropdownOpen()) {
|
||||
eventHub.$emit('clickedDropdown');
|
||||
this.isLoading = true;
|
||||
this.fetchJobs();
|
||||
}
|
||||
},
|
||||
fetchJobs() {
|
||||
axios
|
||||
.get(this.stage.dropdown_path)
|
||||
|
@ -102,133 +80,60 @@ export default {
|
|||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
if (this.isCiMiniPipelineGlDropdown) {
|
||||
this.$refs.stageGlDropdown.hide();
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
this.$refs.stageGlDropdown.hide();
|
||||
this.isLoading = false;
|
||||
|
||||
Flash(__('Something went wrong on our end.'));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When the user right clicks or cmd/ctrl + click in the job name
|
||||
* the dropdown should not be closed and the link should open in another tab,
|
||||
* so we stop propagation of the click event inside the dropdown.
|
||||
*
|
||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||
* target the click event of this component.
|
||||
*
|
||||
* Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(
|
||||
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
|
||||
this.$el,
|
||||
).on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
closeDropdown() {
|
||||
if (this.isDropdownOpen()) {
|
||||
$(this.$refs.dropdown).dropdown('toggle');
|
||||
}
|
||||
},
|
||||
isDropdownOpen() {
|
||||
return this.$el.classList.contains('show');
|
||||
},
|
||||
pipelineActionRequestComplete() {
|
||||
if (this.type === PIPELINES_TABLE) {
|
||||
// warn the table to update
|
||||
// warn the pipelines table to update
|
||||
eventHub.$emit('refreshPipelinesTable');
|
||||
return;
|
||||
}
|
||||
// close the dropdown in mr widget
|
||||
if (this.isCiMiniPipelineGlDropdown) {
|
||||
this.$refs.stageGlDropdown.hide();
|
||||
} else {
|
||||
$(this.$refs.dropdown).dropdown('toggle');
|
||||
}
|
||||
// close the dropdown in MR widget
|
||||
this.$refs.stageGlDropdown.hide();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<gl-dropdown
|
||||
v-if="isCiMiniPipelineGlDropdown"
|
||||
ref="stageGlDropdown"
|
||||
v-gl-tooltip.hover
|
||||
data-testid="mini-pipeline-graph-dropdown"
|
||||
:title="stage.title"
|
||||
variant="link"
|
||||
:lazy="true"
|
||||
:popper-opts="{ placement: 'bottom' }"
|
||||
:toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]"
|
||||
menu-class="mini-pipeline-graph-dropdown-menu"
|
||||
@show="onShowDropdown"
|
||||
>
|
||||
<template #button-content>
|
||||
<span class="gl-pointer-events-none">
|
||||
<gl-icon :name="borderlessIcon" />
|
||||
</span>
|
||||
</template>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<ul
|
||||
v-else
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
data-testid="mini-pipeline-graph-dropdown-menu-list"
|
||||
>
|
||||
<li v-for="job in dropdownContent" :key="job.id">
|
||||
<job-item
|
||||
:dropdown-length="dropdownContent.length"
|
||||
:job="job"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</gl-dropdown>
|
||||
|
||||
<template v-else>
|
||||
<button
|
||||
id="stageDropdown"
|
||||
ref="dropdown"
|
||||
v-gl-tooltip.hover
|
||||
:class="triggerButtonClass"
|
||||
:title="stage.title"
|
||||
class="mini-pipeline-graph-dropdown-toggle"
|
||||
data-testid="mini-pipeline-graph-dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
data-display="static"
|
||||
type="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
@click="onClickStage"
|
||||
>
|
||||
<span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
|
||||
<gl-icon :name="borderlessIcon" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
|
||||
aria-labelledby="stageDropdown"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<ul v-else class="js-builds-dropdown-list scrollable-menu">
|
||||
<li v-for="job in dropdownContent" :key="job.id">
|
||||
<job-item
|
||||
:dropdown-length="dropdownContent.length"
|
||||
:job="job"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<gl-dropdown
|
||||
ref="stageGlDropdown"
|
||||
v-gl-tooltip.hover
|
||||
data-testid="mini-pipeline-graph-dropdown"
|
||||
:title="stage.title"
|
||||
variant="link"
|
||||
:lazy="true"
|
||||
:popper-opts="{ placement: 'bottom' }"
|
||||
:toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
|
||||
menu-class="mini-pipeline-graph-dropdown-menu"
|
||||
@show="onShowDropdown"
|
||||
>
|
||||
<template #button-content>
|
||||
<span class="gl-pointer-events-none">
|
||||
<gl-icon :name="borderlessIcon" />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<ul
|
||||
v-else
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
data-testid="mini-pipeline-graph-dropdown-menu-list"
|
||||
>
|
||||
<li v-for="job in dropdownContent" :key="job.id">
|
||||
<job-item
|
||||
:dropdown-length="dropdownContent.length"
|
||||
:job="job"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import UserList from '~/user_lists/components/user_list.vue';
|
||||
import createStore from '~/user_lists/store/show';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default function featureFlagsUserListInit() {
|
||||
const el = document.getElementById('js-edit-user-list');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store: createStore(el.dataset),
|
||||
render(h) {
|
||||
const { emptyStatePath } = el.dataset;
|
||||
return h(UserList, { props: { emptyStatePath } });
|
||||
},
|
||||
});
|
||||
}
|
|
@ -37,7 +37,6 @@ export default {
|
|||
v-for="(listItem, index) in images"
|
||||
:key="index"
|
||||
:item="listItem"
|
||||
:first="index === 0"
|
||||
:metadata-loading="metadataLoading"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
LIST_INTRO_TEXT,
|
||||
EXPIRATION_POLICY_WILL_RUN_IN,
|
||||
EXPIRATION_POLICY_DISABLED_TEXT,
|
||||
EXPIRATION_POLICY_DISABLED_MESSAGE,
|
||||
} from '../../constants/index';
|
||||
|
||||
export default {
|
||||
|
@ -34,11 +33,6 @@ export default {
|
|||
default: '',
|
||||
required: false,
|
||||
},
|
||||
expirationPolicyHelpPagePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
hideExpirationPolicyData: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -79,19 +73,8 @@ export default {
|
|||
? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
|
||||
: EXPIRATION_POLICY_DISABLED_TEXT;
|
||||
},
|
||||
showExpirationPolicyTip() {
|
||||
return (
|
||||
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
|
||||
);
|
||||
},
|
||||
infoMessages() {
|
||||
const base = [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
|
||||
return this.showExpirationPolicyTip
|
||||
? [
|
||||
...base,
|
||||
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: this.expirationPolicyHelpPagePath },
|
||||
]
|
||||
: base;
|
||||
return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,9 +6,6 @@ export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
|
|||
export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
|
||||
'ContainerRegistry|Expiration policy is disabled',
|
||||
);
|
||||
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
|
||||
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
|
||||
);
|
||||
export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted');
|
||||
export const DELETE_ALERT_LINK_TEXT = s__(
|
||||
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',
|
||||
|
|
|
@ -288,7 +288,6 @@ export default {
|
|||
:images-count="containerRepositoriesCount"
|
||||
:expiration-policy="config.expirationPolicy"
|
||||
:help-page-path="config.helpPagePath"
|
||||
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
|
||||
:hide-expiration-policy-data="config.isGroupPage"
|
||||
>
|
||||
<template #commands>
|
||||
|
|
|
@ -54,7 +54,7 @@ export default {
|
|||
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
|
||||
:class="optionalClasses"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center gl-py-3">
|
||||
<div class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5">
|
||||
<div
|
||||
v-if="$slots['left-action']"
|
||||
class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
|
||||
|
|
|
@ -11,7 +11,12 @@
|
|||
.dropdown-menu.show {
|
||||
// Make the dropdown a little wider and longer than usual
|
||||
// since it contains quite a bit of content.
|
||||
overflow: hidden;
|
||||
width: 20rem;
|
||||
max-height: $dropdown-max-height-lg;
|
||||
|
||||
&,
|
||||
.gl-new-dropdown-inner {
|
||||
max-height: $dropdown-max-height-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,8 +67,7 @@
|
|||
// Mini Pipelines
|
||||
|
||||
.stage-cell {
|
||||
.mini-pipeline-graph-dropdown-toggle,
|
||||
.mini-pipeline-graph-gl-dropdown-toggle {
|
||||
.mini-pipeline-graph-dropdown-toggle {
|
||||
svg {
|
||||
height: $ci-action-icon-size;
|
||||
width: $ci-action-icon-size;
|
||||
|
@ -138,14 +137,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Dropdown button in mini pipeline graph
|
||||
// Commit mini pipeline (HAML)
|
||||
button.mini-pipeline-graph-dropdown-toggle,
|
||||
// As the `mini-pipeline-item` mixin specificity is lower
|
||||
// than the toggle of dropdown with 'variant="link"' we add
|
||||
// classes ".gl-button.btn-link" to make it more specific.
|
||||
// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
|
||||
// itself could increase its specificity to simplify this selector
|
||||
button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle {
|
||||
// GlDropdown mini pipeline (Vue)
|
||||
// As the `mini-pipeline-item` mixin specificity is lower
|
||||
// than the toggle of dropdown with 'variant="link"' we add
|
||||
// classes ".gl-button.btn-link" to make it more specific
|
||||
// and avoid having the size overriden
|
||||
//
|
||||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737
|
||||
button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle {
|
||||
@include mini-pipeline-item();
|
||||
}
|
||||
|
||||
|
|
|
@ -226,10 +226,6 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
}
|
||||
|
||||
.mini-pipeline-graph-dropdown-toggle {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.normal {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
|
@ -982,15 +978,15 @@ $tabs-holder-z-index: 250;
|
|||
line-height: initial;
|
||||
}
|
||||
|
||||
.mini-pipeline-graph-dropdown-toggle,
|
||||
.stage-cell .mini-pipeline-graph-dropdown-toggle svg,
|
||||
// As the `mini-pipeline-item` mixin specificity is lower
|
||||
// than the toggle of dropdown with 'variant="link"' we add
|
||||
// classes ".gl-button.btn-link" to make it more specific.
|
||||
// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
|
||||
// itself could increase its specificity to simplify this selector
|
||||
button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle,
|
||||
.stage-cell button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle svg {
|
||||
// GlDropdown mini pipeline (Vue)
|
||||
// As the `mini-pipeline-item` mixin specificity is lower
|
||||
// than the toggle of dropdown with 'variant="link"' we add
|
||||
// classes ".gl-button.btn-link" to make it more specific
|
||||
// and avoid having the size overriden
|
||||
//
|
||||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737
|
||||
button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle,
|
||||
.stage-cell button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle svg {
|
||||
height: $ci-action-icon-size-lg;
|
||||
width: $ci-action-icon-size-lg;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,18 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
|
|||
result = DependencyProxy::FindOrCreateManifestService.new(group, image, tag, token).execute
|
||||
|
||||
if result[:status] == :success
|
||||
send_upload(result[:manifest].file)
|
||||
response.headers['Docker-Content-Digest'] = result[:manifest].digest
|
||||
response.headers['Content-Length'] = result[:manifest].size
|
||||
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
|
||||
response.headers['Etag'] = "\"#{result[:manifest].digest}\""
|
||||
content_type = result[:manifest].content_type
|
||||
|
||||
send_upload(
|
||||
result[:manifest].file,
|
||||
proxy: true,
|
||||
redirect_params: { query: { 'response-content-type' => content_type } },
|
||||
send_params: { type: content_type }
|
||||
)
|
||||
else
|
||||
render status: result[:http_status], json: result[:message]
|
||||
end
|
||||
|
|
|
@ -37,8 +37,13 @@ class Import::BulkImportsController < ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
result = BulkImportService.new(current_user, create_params, credentials).execute
|
||||
render json: result.to_json(only: [:id])
|
||||
response = BulkImportService.new(current_user, create_params, credentials).execute
|
||||
|
||||
if response.success?
|
||||
render json: response.payload.to_json(only: [:id])
|
||||
else
|
||||
render json: { error: response.message }, status: response.http_status
|
||||
end
|
||||
end
|
||||
|
||||
def realtime_changes
|
||||
|
|
|
@ -18,9 +18,6 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
before_action :define_commit_vars, only: [:show, :diff_for_path, :diff_files, :pipelines, :merge_requests]
|
||||
before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files]
|
||||
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
|
||||
before_action only: [:pipelines] do
|
||||
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
BRANCH_SEARCH_LIMIT = 1000
|
||||
COMMIT_DIFFS_PER_PAGE = 75
|
||||
|
|
|
@ -44,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:suggestions_custom_commit, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml)
|
||||
|
||||
record_experiment_user(:invite_members_version_a)
|
||||
record_experiment_user(:invite_members_version_b)
|
||||
|
|
|
@ -17,7 +17,6 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, project, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
|
||||
end
|
||||
before_action :ensure_pipeline, only: [:show]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
module DependencyProxy
|
||||
URL_SUFFIX = '/dependency_proxy/containers'
|
||||
DISTRIBUTION_API_VERSION = 'registry/2.0'
|
||||
|
||||
def self.table_name_prefix
|
||||
'dependency_proxy_'
|
||||
|
|
|
@ -12,5 +12,10 @@ class DependencyProxy::Manifest < ApplicationRecord
|
|||
|
||||
mount_file_store_uploader DependencyProxy::FileUploader
|
||||
|
||||
scope :find_or_initialize_by_file_name, ->(file_name) { find_or_initialize_by(file_name: file_name) }
|
||||
def self.find_or_initialize_by_file_name_or_digest(file_name:, digest:)
|
||||
result = find_by(file_name: file_name) || find_by(digest: digest)
|
||||
return result if result
|
||||
|
||||
new(file_name: file_name, digest: digest)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -388,7 +388,7 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
def user_ids_for_project_authorizations
|
||||
members_with_parents.pluck(:user_id)
|
||||
members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
|
||||
end
|
||||
|
||||
def self_and_ancestors_ids
|
||||
|
|
|
@ -345,7 +345,7 @@ class Project < ApplicationRecord
|
|||
|
||||
has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
|
||||
|
||||
has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove', inverse_of: :container
|
||||
has_many :repository_storage_moves, class_name: 'Projects::RepositoryStorageMove', inverse_of: :container
|
||||
|
||||
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
|
||||
has_many :reviews, inverse_of: :project
|
||||
|
|
|
@ -1,34 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# ProjectRepositoryStorageMove are details of repository storage moves for a
|
||||
# project. For example, moving a project to another gitaly node to help
|
||||
# balance storage capacity.
|
||||
class ProjectRepositoryStorageMove < ApplicationRecord
|
||||
extend ::Gitlab::Utils::Override
|
||||
include RepositoryStorageMovable
|
||||
|
||||
belongs_to :container, class_name: 'Project', inverse_of: :repository_storage_moves, foreign_key: :project_id
|
||||
alias_attribute :project, :container
|
||||
scope :with_projects, -> { includes(container: :route) }
|
||||
|
||||
override :update_repository_storage
|
||||
def update_repository_storage(new_storage)
|
||||
container.update_column(:repository_storage, new_storage)
|
||||
end
|
||||
|
||||
override :schedule_repository_storage_update_worker
|
||||
def schedule_repository_storage_update_worker
|
||||
ProjectUpdateRepositoryStorageWorker.perform_async(
|
||||
project_id,
|
||||
destination_storage_name,
|
||||
id
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
override :error_key
|
||||
def error_key
|
||||
:project
|
||||
end
|
||||
# This is a compatibility class to avoid calling a non-existent
|
||||
# class from sidekiq during deployment.
|
||||
#
|
||||
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
|
||||
# we cannot remove this class entirely because there can be jobs
|
||||
# referencing it.
|
||||
#
|
||||
# We can get rid of this class in 14.0
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
|
||||
class ProjectRepositoryStorageMove < Projects::RepositoryStorageMove
|
||||
end
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Projects::RepositoryStorageMove are details of repository storage moves for a
|
||||
# project. For example, moving a project to another gitaly node to help
|
||||
# balance storage capacity.
|
||||
module Projects
|
||||
class RepositoryStorageMove < ApplicationRecord
|
||||
extend ::Gitlab::Utils::Override
|
||||
include RepositoryStorageMovable
|
||||
|
||||
self.table_name = 'project_repository_storage_moves'
|
||||
|
||||
belongs_to :container, class_name: 'Project', inverse_of: :repository_storage_moves, foreign_key: :project_id
|
||||
alias_attribute :project, :container
|
||||
scope :with_projects, -> { includes(container: :route) }
|
||||
|
||||
override :update_repository_storage
|
||||
def update_repository_storage(new_storage)
|
||||
container.update_column(:repository_storage, new_storage)
|
||||
end
|
||||
|
||||
override :schedule_repository_storage_update_worker
|
||||
def schedule_repository_storage_update_worker
|
||||
Projects::UpdateRepositoryStorageWorker.perform_async(
|
||||
project_id,
|
||||
destination_storage_name,
|
||||
id
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
override :error_key
|
||||
def error_key
|
||||
:project
|
||||
end
|
||||
end
|
||||
end
|
|
@ -39,7 +39,12 @@ class BulkImportService
|
|||
|
||||
BulkImportWorker.perform_async(bulk_import.id)
|
||||
|
||||
bulk_import
|
||||
ServiceResponse.success(payload: bulk_import)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
ServiceResponse.error(
|
||||
message: e.message,
|
||||
http_status: :unprocessable_entity
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -13,7 +13,7 @@ module DependencyProxy
|
|||
|
||||
def execute
|
||||
@manifest = @group.dependency_proxy_manifests
|
||||
.find_or_initialize_by_file_name(@file_name)
|
||||
.find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
|
||||
|
||||
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
|
||||
|
||||
|
@ -30,6 +30,7 @@ module DependencyProxy
|
|||
def pull_new_manifest
|
||||
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
|
||||
@manifest.update!(
|
||||
content_type: new_manifest[:content_type],
|
||||
digest: new_manifest[:digest],
|
||||
file: new_manifest[:file],
|
||||
size: new_manifest[:file].size
|
||||
|
@ -38,7 +39,9 @@ module DependencyProxy
|
|||
end
|
||||
|
||||
def cached_manifest_matches?(head_result)
|
||||
@manifest && @manifest.digest == head_result[:digest]
|
||||
return false if head_result[:status] == :error
|
||||
|
||||
@manifest && @manifest.digest == head_result[:digest] && @manifest.content_type == head_result[:content_type]
|
||||
end
|
||||
|
||||
def respond
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module DependencyProxy
|
||||
class HeadManifestService < DependencyProxy::BaseService
|
||||
ACCEPT_HEADERS = ::ContainerRegistry::Client::ACCEPTED_TYPES.join(',')
|
||||
|
||||
def initialize(image, tag, token)
|
||||
@image = image
|
||||
@tag = tag
|
||||
|
@ -9,10 +11,10 @@ module DependencyProxy
|
|||
end
|
||||
|
||||
def execute
|
||||
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers)
|
||||
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
|
||||
|
||||
if response.success?
|
||||
success(digest: response.headers['docker-content-digest'])
|
||||
success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
|
||||
else
|
||||
error(response.body, response.code)
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module DependencyProxy
|
|||
def execute_with_manifest
|
||||
raise ArgumentError, 'Block must be provided' unless block_given?
|
||||
|
||||
response = Gitlab::HTTP.get(manifest_url, headers: auth_headers)
|
||||
response = Gitlab::HTTP.get(manifest_url, headers: auth_headers.merge(Accept: ::ContainerRegistry::Client::ACCEPTED_TYPES.join(',')))
|
||||
|
||||
if response.success?
|
||||
file = Tempfile.new
|
||||
|
@ -20,7 +20,7 @@ module DependencyProxy
|
|||
file.write(response)
|
||||
file.flush
|
||||
|
||||
yield(success(file: file, digest: response.headers['docker-content-digest']))
|
||||
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
|
||||
ensure
|
||||
file.close
|
||||
file.unlink
|
||||
|
|
|
@ -25,7 +25,7 @@ module Projects
|
|||
|
||||
override :schedule_bulk_worker_klass
|
||||
def self.schedule_bulk_worker_klass
|
||||
::ProjectScheduleBulkRepositoryShardMovesWorker
|
||||
::Projects::ScheduleBulkRepositoryShardMovesWorker
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class DependencyProxy::FileUploader < GitlabUploader
|
||||
include ObjectStorage::Concern
|
||||
|
||||
before :cache, :set_content_type
|
||||
storage_options Gitlab.config.dependency_proxy
|
||||
|
||||
alias_method :upload, :model
|
||||
|
@ -17,6 +18,17 @@ class DependencyProxy::FileUploader < GitlabUploader
|
|||
|
||||
private
|
||||
|
||||
# Docker manifests return a custom content type
|
||||
# GCP will only use the content-type that is stored with the file
|
||||
# and will not allow it to be overwritten when downloaded
|
||||
# so we must store the custom content type in object storage.
|
||||
# This does not apply to DependencyProxy::Blob uploads.
|
||||
def set_content_type(file)
|
||||
return unless model.class == DependencyProxy::Manifest
|
||||
|
||||
file.content_type = model.content_type
|
||||
end
|
||||
|
||||
def dynamic_segment
|
||||
Gitlab::HashedPath.new('dependency_proxy', model.group_id, 'files', model.id, root_hash: model.group_id)
|
||||
end
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"registry_host_url_with_port" => escape_once(registry_config.host_port),
|
||||
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
|
||||
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
|
||||
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
|
||||
"is_admin": current_user&.admin.to_s,
|
||||
is_group_page: "true",
|
||||
"group_path": @group.full_path,
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
|
||||
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
|
||||
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
|
||||
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
|
||||
"project_path": @project.full_path,
|
||||
"gid_prefix": container_repository_gid_prefix,
|
||||
"is_admin": current_user&.admin.to_s,
|
||||
|
|
|
@ -2020,6 +2020,22 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: projects_schedule_bulk_repository_shard_moves
|
||||
:feature_category: :gitaly
|
||||
:has_external_dependencies:
|
||||
:urgency: :throttled
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: projects_update_repository_storage
|
||||
:feature_category: :gitaly
|
||||
:has_external_dependencies:
|
||||
:urgency: :throttled
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: prometheus_create_default_alerts
|
||||
:feature_category: :incident_management
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectScheduleBulkRepositoryShardMovesWorker
|
||||
include ApplicationWorker
|
||||
|
||||
# This is a compatibility class to avoid calling a non-existent
|
||||
# class from sidekiq during deployment.
|
||||
#
|
||||
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
|
||||
# we cannot remove this class entirely because there can be jobs
|
||||
# referencing it.
|
||||
#
|
||||
# We can get rid of this class in 14.0
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
|
||||
class ProjectScheduleBulkRepositoryShardMovesWorker < Projects::ScheduleBulkRepositoryShardMovesWorker
|
||||
idempotent!
|
||||
feature_category :gitaly
|
||||
urgency :throttled
|
||||
|
||||
def perform(source_storage_name, destination_storage_name = nil)
|
||||
Projects::ScheduleBulkRepositoryShardMovesService.new.execute(source_storage_name, destination_storage_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
extend ::Gitlab::Utils::Override
|
||||
include UpdateRepositoryStorageWorker
|
||||
|
||||
private
|
||||
|
||||
override :find_repository_storage_move
|
||||
def find_repository_storage_move(repository_storage_move_id)
|
||||
ProjectRepositoryStorageMove.find(repository_storage_move_id)
|
||||
end
|
||||
|
||||
override :find_container
|
||||
def find_container(container_id)
|
||||
Project.find(container_id)
|
||||
end
|
||||
|
||||
override :update_repository_storage
|
||||
def update_repository_storage(repository_storage_move)
|
||||
::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute
|
||||
end
|
||||
# This is a compatibility class to avoid calling a non-existent
|
||||
# class from sidekiq during deployment.
|
||||
#
|
||||
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
|
||||
# we cannot remove this class entirely because there can be jobs
|
||||
# referencing it.
|
||||
#
|
||||
# We can get rid of this class in 14.0
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
|
||||
class ProjectUpdateRepositoryStorageWorker < Projects::UpdateRepositoryStorageWorker
|
||||
idempotent!
|
||||
urgency :throttled
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class ScheduleBulkRepositoryShardMovesWorker
|
||||
include ApplicationWorker
|
||||
|
||||
idempotent!
|
||||
feature_category :gitaly
|
||||
urgency :throttled
|
||||
|
||||
def perform(source_storage_name, destination_storage_name = nil)
|
||||
Projects::ScheduleBulkRepositoryShardMovesService.new.execute(source_storage_name, destination_storage_name)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class UpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
extend ::Gitlab::Utils::Override
|
||||
include ::UpdateRepositoryStorageWorker
|
||||
|
||||
private
|
||||
|
||||
override :find_repository_storage_move
|
||||
def find_repository_storage_move(repository_storage_move_id)
|
||||
::Projects::RepositoryStorageMove.find(repository_storage_move_id)
|
||||
end
|
||||
|
||||
override :find_container
|
||||
def find_container(container_id)
|
||||
Project.find(container_id)
|
||||
end
|
||||
|
||||
override :update_repository_storage
|
||||
def update_repository_storage(repository_storage_move)
|
||||
::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve at.js members autocomplete matching
|
||||
merge_request: 54681
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove Expiration Policy text from container registry header
|
||||
merge_request: 54665
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refine Registry Lists and Search Bar UI
|
||||
merge_request: 54549
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Consider only distinct user ids for project authorizations refresh jobs for
|
||||
group members
|
||||
merge_request: 54697
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix double scrollbar in ref selector dropdown
|
||||
merge_request: 54719
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change the order of action buttons in the configure feature flags modal
|
||||
merge_request: 54731
|
||||
author:
|
||||
type: changed
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_mini_pipeline_gl_dropdown
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52821
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300400
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
default_enabled: true
|
|
@ -1,9 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
def max_puma_workers
|
||||
Puma.cli_config.options[:workers].to_i
|
||||
end
|
||||
|
||||
if Gitlab::Runtime.puma? && !Gitlab::Runtime.puma_in_clustered_mode?
|
||||
raise 'Puma is only supported in Clustered mode (workers > 0)' if Gitlab.com?
|
||||
|
||||
|
|
|
@ -282,6 +282,10 @@
|
|||
- 1
|
||||
- - projects_git_garbage_collect
|
||||
- 1
|
||||
- - projects_schedule_bulk_repository_shard_moves
|
||||
- 1
|
||||
- - projects_update_repository_storage
|
||||
- 1
|
||||
- - prometheus_create_default_alerts
|
||||
- 1
|
||||
- - propagate_integration
|
||||
|
|
|
@ -10,16 +10,16 @@ class BackfillUpdatedAtAfterRepositoryStorageMove < ActiveRecord::Migration[6.0]
|
|||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class ProjectRepositoryStorageMove < ActiveRecord::Base
|
||||
class RepositoryStorageMove < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'project_repository_storage_moves'
|
||||
end
|
||||
|
||||
def up
|
||||
ProjectRepositoryStorageMove.reset_column_information
|
||||
RepositoryStorageMove.reset_column_information
|
||||
|
||||
ProjectRepositoryStorageMove.select(:project_id).distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
|
||||
RepositoryStorageMove.select(:project_id).distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
|
||||
migrate_in(
|
||||
INTERVAL * index,
|
||||
MIGRATION_CLASS,
|
||||
|
|
|
@ -406,6 +406,8 @@ all the App nodes and Sidekiq nodes.
|
|||
|
||||
#### Using Pages with reduced authentication scope
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/423) in GitLab 13.10.
|
||||
|
||||
By default, the Pages daemon uses the `api` scope to authenticate. You can configure this. For
|
||||
example, this reduces the scope to `read_api` in `/etc/gitlab/gitlab.rb`:
|
||||
|
||||
|
|
|
@ -125,7 +125,9 @@ they have the following privileges:
|
|||
## Deployment-only access to protected environments
|
||||
|
||||
Users granted access to a protected environment, but not push or merge access
|
||||
to the branch deployed to it, are only granted access to deploy the environment.
|
||||
to the branch deployed to it, are only granted access to deploy the environment. An individual in a
|
||||
group with the Reporter permission, or in groups added to the project with Reporter permissions,
|
||||
appears in the dropdown menu for deployment-only access.
|
||||
|
||||
Note that deployment-only access is the only possible access level for users with
|
||||
[Reporter permissions](../../user/permissions.md).
|
||||
|
|
|
@ -59,7 +59,7 @@ Before you push your changes, Lefthook automatically runs the following checks:
|
|||
- ES lint: Run `yarn run internal:eslint` checks (with the [`.eslintrc.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.eslintrc.yml) configuration) on the modified `*.{js,vue}` files. Tags: `frontend`, `style`.
|
||||
- HAML lint: Run `bundle exec haml-lint` checks (with the [`.haml-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.haml-lint.yml) configuration) on the modified `*.html.haml` files. Tags: `view`, `haml`, `style`.
|
||||
- Markdown lint: Run `yarn markdownlint` checks on the modified `*.md` files. Tags: `documentation`, `style`.
|
||||
- SCSS lint: Run `yarn stylelint` checks (with the [`.stylelintrc`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.stylelintrc) configuration) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`.
|
||||
- SCSS lint: Run `yarn lint:stylelint` checks (with the [`.stylelintrc`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.stylelintrc) configuration) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`.
|
||||
- RuboCop: Run `bundle exec rubocop` checks (with the [`.rubocop.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.rubocop.yml) configuration) on the modified `*.rb` files. Tags: `backend`, `style`.
|
||||
- Vale: Run `vale` checks (with the [`.vale.ini`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.vale.ini) configuration) on the modified `*.md` files. Tags: `documentation`, `style`.
|
||||
|
||||
|
|
|
@ -601,6 +601,7 @@ Follow these guidelines for punctuation:
|
|||
|
||||
| Rule | Example |
|
||||
|------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| Avoid semicolons. Use two sentences instead. | _That's the way that the world goes 'round. You're up one day and the next you're down._
|
||||
| Always end full sentences with a period. | _For a complete overview, read through this document._ |
|
||||
| Always add a space after a period when beginning a new sentence. | _For a complete overview, check this doc. For other references, check out this guide._ |
|
||||
| Do not use double spaces. (Tested in [`SentenceSpacing.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SentenceSpacing.yml).) | --- |
|
||||
|
|
|
@ -135,7 +135,7 @@ Before adding a new variable for a color or a size, guarantee:
|
|||
We use [stylelint](https://stylelint.io) to check for style guide conformity. It uses the
|
||||
ruleset in `.stylelintrc` and rules from [our SCSS configuration](https://gitlab.com/gitlab-org/frontend/gitlab-stylelint-config). `.stylelintrc` is located in the home directory of the project.
|
||||
|
||||
To check if any warnings are produced by your changes, run `yarn stylelint` in the GitLab directory. Stylelint also runs in GitLab CI/CD to
|
||||
To check if any warnings are produced by your changes, run `yarn lint:stylelint` in the GitLab directory. Stylelint also runs in GitLab CI/CD to
|
||||
catch any warnings.
|
||||
|
||||
If the Rake task is throwing warnings you don't understand, SCSS Lint's
|
||||
|
|
|
@ -6,20 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Packages
|
||||
|
||||
This document guides you through adding another [package management system](../administration/packages/index.md) support to GitLab.
|
||||
This document guides you through adding support to GitLab for a new a [package management system](../administration/packages/index.md).
|
||||
|
||||
See already supported package types in [Packages documentation](../administration/packages/index.md)
|
||||
See the already supported formats in the [Packages & Registries documentation](../user/packages/index.md)
|
||||
|
||||
Since GitLab packages' UI is pretty generic, it is possible to add basic new
|
||||
package system support with solely backend changes. This guide is superficial and does
|
||||
not cover the way the code should be written. However, you can find a good example
|
||||
by looking at the following merge requests:
|
||||
It is possible to add a new format with only backend changes.
|
||||
This guide is superficial and does not cover the way the code should be written.
|
||||
However, you can find a good example by looking at the following merge requests:
|
||||
|
||||
- [npm registry support](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8673).
|
||||
- [Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6607).
|
||||
- [Composer repository for PHP dependencies](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22415).
|
||||
- [Terraform modules registry](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834).
|
||||
- [Instance-level endpoint for Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8757).
|
||||
- [npm registry support](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8673)
|
||||
- [Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6607)
|
||||
- [Instance-level API for Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8757)
|
||||
- [NuGet group-level API](https://gitlab.com/gitlab-org/gitlab/-/issues/36423)
|
||||
|
||||
## General information
|
||||
|
||||
|
@ -60,26 +58,13 @@ project are visible. Alternatively, a group-level endpoint may be used to allow
|
|||
within a given group. Lastly, an instance-level endpoint can be used to allow visibility to all packages within an
|
||||
entire GitLab instance.
|
||||
|
||||
Using group and project level endpoints allows for more flexibility in package naming, however, more remotes
|
||||
have to be managed. Using instance level endpoints requires [stricter naming conventions](#naming-conventions).
|
||||
As an MVC, we recommend beginning with a project-level endpoint. A typical iteration plan for remote hierarchies is to go from:
|
||||
|
||||
The current state of existing package registries availability is:
|
||||
- Publish and install in a project
|
||||
- Install from a group
|
||||
- Publish and install in an Instance (this is for Self-Managed customers)
|
||||
|
||||
| Repository Type | Project Level | Group Level | Instance Level |
|
||||
|------------------|---------------|-------------|----------------|
|
||||
| Maven | Yes | Yes | Yes |
|
||||
| Conan | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) | Yes |
|
||||
| npm | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) |
|
||||
| NuGet | Yes | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36425) |
|
||||
| PyPI | Yes | No | No |
|
||||
| Go | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213900) | No - [open-issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213902) |
|
||||
| Composer | Yes | Yes | No |
|
||||
| Generic | Yes | No | No |
|
||||
|
||||
NOTE:
|
||||
npm is currently a hybrid of the instance level and group level.
|
||||
It is using the top-level group or namespace as the defining portion of the name
|
||||
(for example, `@my-group-name/my-package-name`).
|
||||
Using instance-level endpoints requires [stricter naming conventions](#naming-conventions).
|
||||
|
||||
NOTE:
|
||||
Composer package naming scope is Instance Level.
|
||||
|
@ -116,8 +101,8 @@ Packages can be configured to use object storage, therefore your code must suppo
|
|||
|
||||
The way new package systems are integrated in GitLab is using an [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc). Therefore, the first iteration should support the bare minimum user actions:
|
||||
|
||||
- Authentication
|
||||
- Uploading a package
|
||||
- Authentication with a GitLab job, personal access, project access, or deploy token
|
||||
- Uploading a package and displaying basic metadata in the user interface
|
||||
- Pulling a package
|
||||
- Required actions
|
||||
|
||||
|
@ -242,6 +227,17 @@ create the package record. Workhorse provides a variety of file metadata such as
|
|||
For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md)
|
||||
in your local development environment.
|
||||
|
||||
#### File size limits
|
||||
|
||||
Files uploaded to the GitLab Package Registry are [limited by format](../administration/instance_limits.md#package-registry-limits).
|
||||
On GitLab.com, these are typically set to 5GB to help prevent timeout issues and abuse.
|
||||
|
||||
When a new package type is added to the `Packages::Package` model, a size limit must be added
|
||||
similar to [this example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52639/diffs#382f879fb09b0212e3cedd99e6c46e2083867216),
|
||||
or the [related test](https://gitlab.com/gitlab-org/gitlab/-/blob/fe4ba43766781371cebfacd78364a1de762917cd/spec/models/packages/package_spec.rb#L761)
|
||||
must be updated if file size limits do not apply. The only reason a size limit does not apply is if
|
||||
the package format does not upload and store package files.
|
||||
|
||||
#### Rate Limits on GitLab.com
|
||||
|
||||
Package manager clients can make rapid requests that exceed the
|
||||
|
|
|
@ -96,20 +96,20 @@ Depending on your target platform, some features might not be available to you.
|
|||
Comprised of a set of [stages](stages.md), Auto DevOps brings these best practices to your
|
||||
project in a simple and automatic way:
|
||||
|
||||
1. [Auto Build](stages.md#auto-build)
|
||||
1. [Auto Test](stages.md#auto-test)
|
||||
1. [Auto Code Quality](stages.md#auto-code-quality)
|
||||
1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
|
||||
1. [Auto Secret Detection](stages.md#auto-secret-detection)
|
||||
1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)**
|
||||
1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)**
|
||||
1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)**
|
||||
1. [Auto Review Apps](stages.md#auto-review-apps)
|
||||
1. [Auto DAST (Dynamic Application Security Testing)](stages.md#auto-dast) **(ULTIMATE)**
|
||||
1. [Auto Deploy](stages.md#auto-deploy)
|
||||
1. [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing) **(PREMIUM)**
|
||||
1. [Auto Monitoring](stages.md#auto-monitoring)
|
||||
1. [Auto Code Intelligence](stages.md#auto-code-intelligence)
|
||||
- [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing)
|
||||
- [Auto Build](stages.md#auto-build)
|
||||
- [Auto Code Intelligence](stages.md#auto-code-intelligence)
|
||||
- [Auto Code Quality](stages.md#auto-code-quality)
|
||||
- [Auto Container Scanning](stages.md#auto-container-scanning)
|
||||
- [Auto DAST (Dynamic Application Security Testing)](stages.md#auto-dast)
|
||||
- [Auto Dependency Scanning](stages.md#auto-dependency-scanning)
|
||||
- [Auto Deploy](stages.md#auto-deploy)
|
||||
- [Auto License Compliance](stages.md#auto-license-compliance)
|
||||
- [Auto Monitoring](stages.md#auto-monitoring)
|
||||
- [Auto Review Apps](stages.md#auto-review-apps)
|
||||
- [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
|
||||
- [Auto Secret Detection](stages.md#auto-secret-detection)
|
||||
- [Auto Test](stages.md#auto-test)
|
||||
|
||||
As Auto DevOps relies on many different components, you should have a basic
|
||||
knowledge of the following:
|
||||
|
@ -334,3 +334,6 @@ spec:
|
|||
- name: https_proxy
|
||||
value: "PUT_YOUR_HTTPS_PROXY_HERE"
|
||||
```
|
||||
|
||||
<!-- DO NOT ADD TROUBLESHOOTING INFO HERE -->
|
||||
<!-- Troubleshooting information has moved to troubleshooting.md -->
|
||||
|
|
|
@ -529,7 +529,8 @@ workers:
|
|||
|
||||
### Network Policy
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/30) in GitLab 12.7.
|
||||
- [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/30) in GitLab 12.7.
|
||||
- [Deprecated](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/184) in GitLab 13.9.
|
||||
|
||||
By default, all Kubernetes pods are
|
||||
[non-isolated](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods),
|
||||
|
@ -580,6 +581,76 @@ networkPolicy:
|
|||
For more information on installing Network Policies, see
|
||||
[Install Cilium using GitLab CI/CD](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd).
|
||||
|
||||
### Cilium Network Policy
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/184) in GitLab 13.9.
|
||||
|
||||
By default, all Kubernetes pods are
|
||||
[non-isolated](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods),
|
||||
and accept traffic to and from any source. You can use
|
||||
[CiliumNetworkPolicy](https://docs.cilium.io/en/v1.8/concepts/kubernetes/policy/#ciliumnetworkpolicy)
|
||||
to restrict connections to and from selected pods, namespaces, and the internet.
|
||||
|
||||
#### Requirements
|
||||
|
||||
As the default network plugin for Kubernetes (`kubenet`)
|
||||
[does not implement](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#kubenet)
|
||||
support for it, you must have [Cilium](https://docs.cilium.io/en/v1.8/intro/) as your Kubernetes network plugin.
|
||||
|
||||
The [Cilium](https://cilium.io/) network plugin can be
|
||||
installed as a [cluster application](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd)
|
||||
to enable support for network policies.
|
||||
|
||||
#### Configuration
|
||||
|
||||
You can enable deployment of a network policy by setting the following
|
||||
in the `.gitlab/auto-deploy-values.yaml` file:
|
||||
|
||||
```yaml
|
||||
ciliumNetworkPolicy:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The default policy deployed by the Auto Deploy pipeline allows
|
||||
traffic within a local namespace, and from the `gitlab-managed-apps`
|
||||
namespace. All other inbound connections are blocked. Outbound
|
||||
traffic (for example, to the internet) is not affected by the default policy.
|
||||
|
||||
You can also provide a custom [policy specification](https://docs.cilium.io/en/v1.8/policy/language/#simple-ingress-allow)
|
||||
in the `.gitlab/auto-deploy-values.yaml` file, for example:
|
||||
|
||||
```yaml
|
||||
ciliumNetworkPolicy:
|
||||
enabled: true
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.gitlab.com/env: staging
|
||||
ingress:
|
||||
- fromEndpoints:
|
||||
- matchLabels:
|
||||
app.gitlab.com/managed_by: gitlab
|
||||
```
|
||||
|
||||
#### Enabling Alerts
|
||||
|
||||
You can also enable alerts. Network policies with alerts are considered only if
|
||||
[GitLab Kubernetes Agent](https://docs.gitlab.com/13.6/ee/user/clusters/agent/)
|
||||
has been integrated.
|
||||
|
||||
You can enable alerts as follows:
|
||||
|
||||
```yaml
|
||||
ciliumNetworkPolicy:
|
||||
enabled: true
|
||||
alerts:
|
||||
enabled: true
|
||||
|
||||
```
|
||||
|
||||
For more information on installing Network Policies, see
|
||||
[Install Cilium using GitLab CI/CD](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd).
|
||||
|
||||
### Web Application Firewall (ModSecurity) customization
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/44) in GitLab 12.8.
|
||||
|
|
|
@ -152,20 +152,20 @@ To set a limit on how long these sessions are valid:
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) in GitLab Ultimate 12.6.
|
||||
|
||||
Users can optionally specify an expiration date for
|
||||
Users can optionally specify a lifetime for
|
||||
[personal access tokens](../../profile/personal_access_tokens.md).
|
||||
This expiration date is not a requirement, and can be set to any arbitrary date.
|
||||
This lifetime is not a requirement, and can be set to any arbitrary number of days.
|
||||
|
||||
Personal access tokens are the only tokens needed for programmatic access to GitLab.
|
||||
However, organizations with security requirements may want to enforce more protection by
|
||||
requiring the regular rotation of these tokens.
|
||||
|
||||
### Setting a limit
|
||||
### Setting a lifetime
|
||||
|
||||
Only a GitLab administrator can set a limit. Leaving it empty means
|
||||
Only a GitLab administrator can set a lifetime. Leaving it empty means
|
||||
there are no restrictions.
|
||||
|
||||
To set a limit on how long personal access tokens are valid:
|
||||
To set a lifetime on how long personal access tokens are valid:
|
||||
|
||||
1. Navigate to **Admin Area > Settings > General**.
|
||||
1. Expand the **Account and limit** section.
|
||||
|
|
|
@ -48,6 +48,7 @@ The following resources are migrated to the target instance:
|
|||
- author ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/298745))
|
||||
- parent epic ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297459))
|
||||
- emoji award ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297466))
|
||||
- events ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/297465))
|
||||
|
||||
Any other items are **not** migrated.
|
||||
|
||||
|
|
|
@ -4,12 +4,13 @@ group: Package
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Dependency Proxy
|
||||
# Dependency Proxy **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
|
||||
> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
|
||||
> - Anonymous access to images in public groups is no longer available starting in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to GitLab Free in GitLab 13.6.
|
||||
> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in GitLab Free 13.7.
|
||||
> - Anonymous access to images in public groups is no longer available starting in GitLab Free 13.7.
|
||||
> - [Support for pull-by-digest and Docker version 20.x](https://gitlab.com/gitlab-org/gitlab/-/issues/290944) in GitLab Free 13.10.
|
||||
|
||||
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
|
||||
upstream images.
|
||||
|
@ -17,11 +18,6 @@ upstream images.
|
|||
In the case of CI/CD, the Dependency Proxy receives a request and returns the
|
||||
upstream image from a registry, acting as a pull-through cache.
|
||||
|
||||
NOTE:
|
||||
The Dependency Proxy is not compatible with Docker version 20.x and later.
|
||||
If you are using the Dependency Proxy, Docker version 19.x.x is recommended until
|
||||
[issue #290944](https://gitlab.com/gitlab-org/gitlab/-/issues/290944) is resolved.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md).
|
||||
|
@ -60,7 +56,7 @@ Prerequisites:
|
|||
|
||||
### Authenticate with the Dependency Proxy
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in GitLab Free 13.7.
|
||||
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
|
@ -162,7 +158,7 @@ the [Dependency Proxy API](../../../api/dependency_proxy.md).
|
|||
|
||||
## Docker Hub rate limits and the Dependency Proxy
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) in GitLab Free 13.7.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
Watch how to [use the Dependency Proxy to help avoid Docker Hub rate limits](https://youtu.be/Nc4nUo7Pq08).
|
||||
|
|
|
@ -15,25 +15,24 @@ Markdown fields. When you start typing a word in a Markdown field with one of
|
|||
the following characters, GitLab progressively autocompletes against a set of
|
||||
matching values. The string matching is not case sensitive.
|
||||
|
||||
| Character | Autocompletes |
|
||||
| :-------- | :------------ |
|
||||
| `~` | Labels |
|
||||
| `%` | Milestones |
|
||||
| `@` | Users and groups |
|
||||
| `#` | Issues |
|
||||
| `!` | Merge requests |
|
||||
| `&` | Epics |
|
||||
| `$` | Snippets |
|
||||
| `:` | Emoji |
|
||||
| `/` | Quick Actions |
|
||||
| Character | Autocompletes | Relevant matches shown |
|
||||
| :-------- | :------------ | :---- |
|
||||
| `~` | Labels | 20 |
|
||||
| `%` | Milestones | 5 |
|
||||
| `@` | Users and groups | 10 |
|
||||
| `#` | Issues | 5 |
|
||||
| `!` | Merge requests | 5 |
|
||||
| `&` | Epics | 5 |
|
||||
| `$` | Snippets | 5 |
|
||||
| `:` | Emoji | 5 |
|
||||
| `/` | Quick Actions | 100 |
|
||||
|
||||
Up to 5 of the most relevant matches are displayed in a popup list. When you
|
||||
select an item from the list, the value is entered in the field. The more
|
||||
characters you enter, the more precise the matches are.
|
||||
When you select an item from the list, the value is entered in the field.
|
||||
The more characters you enter, the more precise the matches are.
|
||||
|
||||
Autocomplete characters are useful when combined with [Quick Actions](quick_actions.md).
|
||||
|
||||
## Example
|
||||
## User autocomplete
|
||||
|
||||
Assume your GitLab instance includes the following users:
|
||||
|
||||
|
@ -49,17 +48,9 @@ Assume your GitLab instance includes the following users:
|
|||
|
||||
<!-- vale gitlab.Spelling = YES -->
|
||||
|
||||
In an Issue comment, entering `@l` results in the following popup list
|
||||
appearing. Note that user `shelba` is not included, because the list includes
|
||||
only the 5 users most relevant to the Issue.
|
||||
|
||||
![Popup list which includes users whose username or name contains the letter `l`](img/autocomplete_characters_example1_v12_0.png)
|
||||
|
||||
If you continue to type, `@le`, the popup list changes to the following. The
|
||||
popup now only includes users where `le` appears in their username, or a word in
|
||||
their name.
|
||||
|
||||
![Popup list which includes users whose username or name contains the string](img/autocomplete_characters_example2_v12_0.png)
|
||||
User autocompletion sorts by the users whose username or name start with your query first.
|
||||
For example, typing `@lea` shows `leanna` first and typing `@ros` shows `Rosemarie Rogahn` and `Rosy Grant` first.
|
||||
Any usernames or names that include your query are shown afterwards in the autocomplete menu.
|
||||
|
||||
You can also search across the full name to find a user.
|
||||
To find `Rosy Grant`, even if their username is for example `hunter2`, you can type their full name without spaces like `@rosygrant`.
|
||||
To find `Rosy Grant`, even if their username is for example `alessandra`, you can type their full name without spaces like `@rosygrant`.
|
||||
|
|
|
@ -475,7 +475,7 @@ terminal.
|
|||
Read the [Release CLI documentation](https://gitlab.com/gitlab-org/release-cli/-/blob/master/docs/index.md)
|
||||
for details.
|
||||
|
||||
## Release Metrics **(PREMIUM)**
|
||||
## Release Metrics **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259703) in GitLab Premium 13.9.
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
|
||||
expose :project, using: Entities::ProjectIdentity
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Projects
|
||||
class RepositoryStorageMove < BasicRepositoryStorageMove
|
||||
expose :project, using: Entities::ProjectIdentity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,28 +11,28 @@ module API
|
|||
resource :project_repository_storage_moves do
|
||||
desc 'Get a list of all project repository storage moves' do
|
||||
detail 'This feature was introduced in GitLab 13.0.'
|
||||
success Entities::ProjectRepositoryStorageMove
|
||||
success Entities::Projects::RepositoryStorageMove
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
get do
|
||||
storage_moves = ProjectRepositoryStorageMove.with_projects.order_created_at_desc
|
||||
storage_moves = ::Projects::RepositoryStorageMove.with_projects.order_created_at_desc
|
||||
|
||||
present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user
|
||||
present paginate(storage_moves), with: Entities::Projects::RepositoryStorageMove, current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Get a project repository storage move' do
|
||||
detail 'This feature was introduced in GitLab 13.0.'
|
||||
success Entities::ProjectRepositoryStorageMove
|
||||
success Entities::Projects::RepositoryStorageMove
|
||||
end
|
||||
params do
|
||||
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a project repository storage move'
|
||||
end
|
||||
get ':repository_storage_move_id' do
|
||||
storage_move = ProjectRepositoryStorageMove.find(params[:repository_storage_move_id])
|
||||
storage_move = ::Projects::RepositoryStorageMove.find(params[:repository_storage_move_id])
|
||||
|
||||
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
|
||||
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Schedule bulk project repository storage moves' do
|
||||
|
@ -58,7 +58,7 @@ module API
|
|||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc 'Get a list of all project repository storage moves' do
|
||||
detail 'This feature was introduced in GitLab 13.1.'
|
||||
success Entities::ProjectRepositoryStorageMove
|
||||
success Entities::Projects::RepositoryStorageMove
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
|
@ -66,12 +66,12 @@ module API
|
|||
get ':id/repository_storage_moves' do
|
||||
storage_moves = user_project.repository_storage_moves.with_projects.order_created_at_desc
|
||||
|
||||
present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user
|
||||
present paginate(storage_moves), with: Entities::Projects::RepositoryStorageMove, current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Get a project repository storage move' do
|
||||
detail 'This feature was introduced in GitLab 13.1.'
|
||||
success Entities::ProjectRepositoryStorageMove
|
||||
success Entities::Projects::RepositoryStorageMove
|
||||
end
|
||||
params do
|
||||
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a project repository storage move'
|
||||
|
@ -79,12 +79,12 @@ module API
|
|||
get ':id/repository_storage_moves/:repository_storage_move_id' do
|
||||
storage_move = user_project.repository_storage_moves.find(params[:repository_storage_move_id])
|
||||
|
||||
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
|
||||
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Schedule a project repository storage move' do
|
||||
detail 'This feature was introduced in GitLab 13.1.'
|
||||
success Entities::ProjectRepositoryStorageMove
|
||||
success Entities::Projects::RepositoryStorageMove
|
||||
end
|
||||
params do
|
||||
optional :destination_storage_name, type: String, desc: 'The destination storage shard'
|
||||
|
@ -95,7 +95,7 @@ module API
|
|||
)
|
||||
|
||||
if storage_move.schedule
|
||||
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
|
||||
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
|
||||
else
|
||||
render_validation_error!(storage_move)
|
||||
end
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Common
|
||||
module Transformers
|
||||
class AwardEmojiTransformer
|
||||
def transform(context, data)
|
||||
user = find_user(context, data&.dig('user', 'public_email')) || context.current_user
|
||||
|
||||
data
|
||||
.except('user')
|
||||
.merge('user_id' => user.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user(context, email)
|
||||
return if email.blank?
|
||||
|
||||
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,6 +15,8 @@ module BulkImports
|
|||
).freeze
|
||||
|
||||
def transform(context, data)
|
||||
return unless data
|
||||
|
||||
data.each_with_object({}) do |(key, value), result|
|
||||
prohibited = prohibited_key?(key)
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# UserReferenceTransformer replaces specified user
|
||||
# reference key with a user id being either:
|
||||
# - A user id found by `public_email` in the group
|
||||
# - Current user id
|
||||
# under a new key `"#{@reference}_id"`.
|
||||
module BulkImports
|
||||
module Common
|
||||
module Transformers
|
||||
class UserReferenceTransformer
|
||||
DEFAULT_REFERENCE = 'user'
|
||||
|
||||
def initialize(options = {})
|
||||
@reference = options[:reference] || DEFAULT_REFERENCE
|
||||
@suffixed_reference = "#{@reference}_id"
|
||||
end
|
||||
|
||||
def transform(context, data)
|
||||
return unless data
|
||||
|
||||
user = find_user(context, data&.dig(@reference, 'public_email')) || context.current_user
|
||||
|
||||
data
|
||||
.except(@reference)
|
||||
.merge(@suffixed_reference => user.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user(context, email)
|
||||
return if email.blank?
|
||||
|
||||
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
module BulkImports
|
||||
module Pipeline
|
||||
extend ActiveSupport::Concern
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include Gitlab::ClassAttributes
|
||||
include Runner
|
||||
|
||||
|
@ -60,12 +61,17 @@ module BulkImports
|
|||
# end
|
||||
# end
|
||||
#
|
||||
# In the example above `MyTransformerOne` is the first to run and
|
||||
# the instance `#transform` method is the last.
|
||||
# In the example above `#transform` is the first to run and
|
||||
# `MyTransformerTwo` method is the last.
|
||||
def transformers
|
||||
@transformers ||= self.class.transformers.map(&method(:instantiate))
|
||||
@transformers << self if respond_to?(:transform) && @transformers.exclude?(self)
|
||||
@transformers
|
||||
strong_memoize(:transformers) do
|
||||
defined_transformers = self.class.transformers.map(&method(:instantiate))
|
||||
|
||||
transformers = []
|
||||
transformers << self if respond_to?(:transform)
|
||||
transformers.concat(defined_transformers)
|
||||
transformers
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch pipeline loader.
|
||||
|
@ -126,7 +132,7 @@ module BulkImports
|
|||
end
|
||||
|
||||
def transformers
|
||||
class_attributes[:transformers]
|
||||
class_attributes[:transformers] || []
|
||||
end
|
||||
|
||||
def get_loader
|
||||
|
|
|
@ -5,7 +5,7 @@ module Gitlab
|
|||
# Update existent project update_at column after their repository storage was moved
|
||||
class BackfillProjectUpdatedAtAfterRepositoryStorageMove
|
||||
def perform(*project_ids)
|
||||
updated_repository_storages = ProjectRepositoryStorageMove.select("project_id, MAX(updated_at) as updated_at").where(project_id: project_ids).group(:project_id)
|
||||
updated_repository_storages = Projects::RepositoryStorageMove.select("project_id, MAX(updated_at) as updated_at").where(project_id: project_ids).group(:project_id)
|
||||
|
||||
Project.connection.execute <<-SQL
|
||||
WITH repository_storage_cte as (
|
||||
|
|
|
@ -82,7 +82,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def puma_in_clustered_mode?
|
||||
puma? && Puma.cli_config.options[:workers].to_i > 0
|
||||
return unless puma?
|
||||
return unless Puma.respond_to?(:cli_config)
|
||||
|
||||
Puma.cli_config.options[:workers].to_i > 0
|
||||
end
|
||||
|
||||
def max_threads
|
||||
|
|
|
@ -7960,9 +7960,6 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Docker connection error"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Expiration policy is disabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17559,9 +17556,6 @@ msgstr ""
|
|||
msgid "Learn more about signing commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more about the dependency list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more in the"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17784,9 +17778,6 @@ msgstr ""
|
|||
msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Licenses|Learn more about license compliance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Licenses|License Compliance"
|
||||
msgstr ""
|
||||
|
||||
|
|
22
package.json
22
package.json
|
@ -5,10 +5,10 @@
|
|||
"block-dependencies": "node scripts/frontend/block_dependencies.js",
|
||||
"clean": "rm -rf public/assets tmp/cache/*-loader",
|
||||
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" node scripts/frontend/webpack_dev_server.js",
|
||||
"eslint-fix": "echo 'Please use lint:eslint:fix instead' && exit 1",
|
||||
"eslint-staged": "echo 'Please use lint:eslint:staged instead' && exit 1",
|
||||
"eslint-staged-fix": "echo 'Please use lint:eslint:staged:fix instead' && exit 1",
|
||||
"eslint-report": "echo 'Please use lint:eslint:report instead' && exit 1",
|
||||
"eslint-fix": "echo 'Please use `yarn lint:eslint:fix` instead' && exit 1",
|
||||
"eslint-staged": "echo 'Please use `yarn lint:eslint:staged` instead' && exit 1",
|
||||
"eslint-staged-fix": "echo 'Please use `yarn lint:eslint:staged:fix` instead' && exit 1",
|
||||
"eslint-report": "echo 'Please use `yarn lint:eslint:report` instead' && exit 1",
|
||||
"file-coverage": "scripts/frontend/file_test_coverage.js",
|
||||
"lint-docs": "scripts/lint-doc.sh",
|
||||
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue",
|
||||
|
@ -30,14 +30,16 @@
|
|||
"lint:prettier:fix": "yarn run prettier --write '**/*.{graphql,js,vue}'",
|
||||
"lint:prettier:staged": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --check",
|
||||
"lint:prettier:staged:fix": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --write",
|
||||
"lint:stylelint": "stylelint --cache -q '{ee/,}app/assets/stylesheets/**/*.{css,scss}'",
|
||||
"lint:stylelint:fix": "yarn run lint:stylelint --fix",
|
||||
"lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
|
||||
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
|
||||
"markdownlint": "markdownlint --config .markdownlint.json",
|
||||
"postinstall": "node ./scripts/frontend/postinstall.js",
|
||||
"prettier-all": "echo 'Please use lint:prettier instead' && exit 1",
|
||||
"prettier-all-save": "echo 'Please use lint:prettier:fix instead' && exit 1",
|
||||
"prettier-staged": "echo 'Please use lint:prettier:staged instead' && exit 1",
|
||||
"prettier-staged-save": "echo 'Please use lint:prettier:staged:fixed instead' && exit 1",
|
||||
"stylelint": "yarn stylelint-file 'app/assets/stylesheets/**/*.*' 'ee/app/assets/stylesheets/**/*.*' '!app/assets/stylesheets/startup/startup-*.scss' '!**/vendors/**'",
|
||||
"stylelint-file": "BROWSERSLIST_IGNORE_OLD_DATA=true node node_modules/stylelint/bin/stylelint.js",
|
||||
"prettier-all": "echo 'Please use `yarn lint:prettier` instead' && exit 1",
|
||||
"prettier-all-save": "echo 'Please use `yarn lint:prettier:fix` instead' && exit 1",
|
||||
"prettier-staged": "echo 'Please use `yarn lint:prettier:staged` instead' && exit 1",
|
||||
"prettier-staged-save": "echo 'Please use `yarn lint:prettier:staged:fix` instead' && exit 1",
|
||||
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
|
||||
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
|
||||
"webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Plan', :smoke, :reliable do
|
||||
# TODO: Remove :requires_admin meta when the `Runtime::Feature.enable` method call is removed
|
||||
RSpec.describe 'Plan', :smoke, :reliable, :requires_admin do
|
||||
describe 'mention' do
|
||||
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
|
||||
let(:project) do
|
||||
|
|
|
@ -33,7 +33,7 @@ class StaticAnalysis
|
|||
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13,
|
||||
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13,
|
||||
%w[bin/rake config_lint] => 11,
|
||||
%w[yarn run stylelint] => 9,
|
||||
%w[yarn run lint:stylelint] => 9,
|
||||
%w[scripts/lint-conflicts.sh] => 0.59,
|
||||
%w[yarn run block-dependencies] => 0.35,
|
||||
%w[scripts/lint-rugged] => 0.23,
|
||||
|
|
|
@ -130,7 +130,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'proxies status from the remote token request' do
|
||||
it 'proxies status from the remote token request', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:service_unavailable)
|
||||
|
@ -147,7 +147,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'proxies status from the remote manifest request' do
|
||||
it 'proxies status from the remote manifest request', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
@ -156,7 +156,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
end
|
||||
|
||||
it 'sends a file' do
|
||||
expect(controller).to receive(:send_file).with(manifest.file.path, {})
|
||||
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -165,6 +165,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
|
||||
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['Etag']).to eq("\"#{manifest.digest}\"")
|
||||
expect(response.headers['Content-Disposition']).to match(/^attachment/)
|
||||
end
|
||||
end
|
||||
|
@ -207,7 +211,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
}
|
||||
end
|
||||
|
||||
it 'proxies status from the remote blob request' do
|
||||
it 'proxies status from the remote blob request', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
@ -221,7 +225,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
subject
|
||||
end
|
||||
|
||||
it 'returns Content-Disposition: attachment' do
|
||||
it 'returns Content-Disposition: attachment', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
|
|
|
@ -184,9 +184,15 @@ RSpec.describe Import::BulkImportsController do
|
|||
end
|
||||
|
||||
describe 'POST create' do
|
||||
let(:instance_url) { "http://fake-intance" }
|
||||
let(:instance_url) { "http://fake-instance" }
|
||||
let(:bulk_import) { create(:bulk_import) }
|
||||
let(:pat) { "fake-pat" }
|
||||
let(:bulk_import_params) do
|
||||
[{ "source_type" => "group_entity",
|
||||
"source_full_path" => "full_path",
|
||||
"destination_name" => "destination_name",
|
||||
"destination_namespace" => "root" }]
|
||||
end
|
||||
|
||||
before do
|
||||
session[:bulk_import_gitlab_access_token] = pat
|
||||
|
@ -194,15 +200,9 @@ RSpec.describe Import::BulkImportsController do
|
|||
end
|
||||
|
||||
it 'executes BulkImportService' do
|
||||
bulk_import_params = [{ "source_type" => "group_entity",
|
||||
"source_full_path" => "full_path",
|
||||
"destination_name" =>
|
||||
"destination_name",
|
||||
"destination_namespace" => "root" }]
|
||||
|
||||
expect_next_instance_of(
|
||||
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
|
||||
allow(service).to receive(:execute).and_return(bulk_import)
|
||||
allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: bulk_import))
|
||||
end
|
||||
|
||||
post :create, params: { bulk_import: bulk_import_params }
|
||||
|
@ -210,6 +210,19 @@ RSpec.describe Import::BulkImportsController do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to eq({ id: bulk_import.id }.to_json)
|
||||
end
|
||||
|
||||
it 'returns error when validation fails' do
|
||||
error_response = ServiceResponse.error(message: 'Record invalid', http_status: :unprocessable_entity)
|
||||
expect_next_instance_of(
|
||||
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
|
||||
allow(service).to receive(:execute).and_return(error_response)
|
||||
end
|
||||
|
||||
post :create, params: { bulk_import: bulk_import_params }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(response.body).to eq({ error: 'Record invalid' }.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ FactoryBot.define do
|
|||
factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do
|
||||
group
|
||||
file { fixture_file_upload('spec/fixtures/dependency_proxy/manifest') }
|
||||
digest { 'sha256:5ab5a6872b264fe4fd35d63991b9b7d8425f4bc79e7cf4d563c10956581170c9' }
|
||||
digest { 'sha256:d0710affa17fad5f466a70159cc458227bd25d4afb39514ef662ead3e6c99515' }
|
||||
file_name { 'alpine:latest.json' }
|
||||
content_type { 'application/vnd.docker.distribution.manifest.v2+json' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :project_repository_storage_move, class: 'ProjectRepositoryStorageMove' do
|
||||
factory :project_repository_storage_move, class: 'Projects::RepositoryStorageMove' do
|
||||
container { association(:project) }
|
||||
|
||||
source_storage_name { 'default' }
|
||||
|
||||
trait :scheduled do
|
||||
state { ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value }
|
||||
state { Projects::RepositoryStorageMove.state_machines[:state].states[:scheduled].value }
|
||||
end
|
||||
|
||||
trait :started do
|
||||
state { ProjectRepositoryStorageMove.state_machines[:state].states[:started].value }
|
||||
state { Projects::RepositoryStorageMove.state_machines[:state].states[:started].value }
|
||||
end
|
||||
|
||||
trait :replicated do
|
||||
state { ProjectRepositoryStorageMove.state_machines[:state].states[:replicated].value }
|
||||
state { Projects::RepositoryStorageMove.state_machines[:state].states[:replicated].value }
|
||||
end
|
||||
|
||||
trait :finished do
|
||||
state { ProjectRepositoryStorageMove.state_machines[:state].states[:finished].value }
|
||||
state { Projects::RepositoryStorageMove.state_machines[:state].states[:finished].value }
|
||||
end
|
||||
|
||||
trait :failed do
|
||||
state { ProjectRepositoryStorageMove.state_machines[:state].states[:failed].value }
|
||||
state { Projects::RepositoryStorageMove.state_machines[:state].states[:failed].value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
let_it_be(:user_xss_title) { 'eve <img src=x onerror=alert(2)<img src=x onerror=alert(1)>' }
|
||||
let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
|
||||
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
|
||||
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
|
||||
let_it_be(:group) { create(:group, name: 'Ancestor') }
|
||||
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
|
||||
let_it_be(:project) { create(:project, group: child_group) }
|
||||
|
@ -16,6 +17,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
before_all do
|
||||
project.add_maintainer(user)
|
||||
project.add_maintainer(user_xss)
|
||||
project.add_maintainer(user2)
|
||||
end
|
||||
|
||||
describe 'when tribute_autocomplete feature flag is off' do
|
||||
|
@ -86,11 +88,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.atwho-container')
|
||||
|
||||
page.within '.atwho-container #at-view-users' do
|
||||
expect(find('li').text).to have_content(user_xss.username)
|
||||
end
|
||||
expect(find_highlighted_autocomplete_item).to have_content(user_xss.username)
|
||||
end
|
||||
|
||||
it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
|
||||
|
@ -190,7 +188,30 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.atwho-view li', visible: true)).to have_content(user.name)
|
||||
expect(find_highlighted_autocomplete_item).to have_content(user.name)
|
||||
end
|
||||
|
||||
it 'shows names that start with the query as the top result' do
|
||||
type(find('#note-body'), '@mar')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
|
||||
end
|
||||
|
||||
it 'shows usernames that start with the query as the top result' do
|
||||
type(find('#note-body'), '@msi')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
|
||||
end
|
||||
|
||||
# Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
|
||||
it 'shows username when pasting then pressing Enter' do
|
||||
fill_in 'Description', with: "@#{user.username}\n"
|
||||
|
||||
expect(find_field('Description').value).to have_content "@#{user.username}"
|
||||
end
|
||||
|
||||
it 'selects the first item for non-assignee dropdowns if a query is entered' do
|
||||
|
@ -1004,4 +1025,8 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def find_highlighted_autocomplete_item
|
||||
find('.atwho-view li.cur', visible: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,156 +9,139 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
|
|||
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') }
|
||||
|
||||
shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
|
||||
dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
|
||||
|
||||
before do
|
||||
build.run
|
||||
build.trace.set('hello')
|
||||
sign_in(user)
|
||||
visit_merge_request
|
||||
end
|
||||
|
||||
def visit_merge_request(format: :html, serializer: nil)
|
||||
visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
|
||||
end
|
||||
|
||||
it 'displays a mini pipeline graph' do
|
||||
expect(page).to have_selector('.mr-widget-pipeline-graph')
|
||||
end
|
||||
|
||||
context 'as json' do
|
||||
let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
|
||||
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
|
||||
|
||||
before do
|
||||
build.run
|
||||
build.trace.set('hello')
|
||||
sign_in(user)
|
||||
stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
|
||||
visit_merge_request
|
||||
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
|
||||
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
|
||||
end
|
||||
|
||||
let_it_be(:dropdown_toggle_selector) do
|
||||
if ci_mini_pipeline_gl_dropdown_enabled
|
||||
'[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
|
||||
else
|
||||
'[data-testid="mini-pipeline-graph-dropdown-toggle"]'
|
||||
end
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
|
||||
xit 'avoids repeated database queries' do
|
||||
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
|
||||
|
||||
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
|
||||
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
|
||||
|
||||
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
|
||||
|
||||
expect(before.count).to eq(after.count)
|
||||
expect(before.cached_count).to eq(after.cached_count)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build list toggle' do
|
||||
let(:toggle) do
|
||||
find(dropdown_selector)
|
||||
first(dropdown_selector)
|
||||
end
|
||||
|
||||
def visit_merge_request(format: :html, serializer: nil)
|
||||
visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
|
||||
# Status icon button styles should update as described in
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
|
||||
it 'has unique styles for default, :hover, :active, and :focus states' do
|
||||
default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_selector)
|
||||
|
||||
toggle.hover
|
||||
hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_selector)
|
||||
|
||||
page.driver.browser.action.click_and_hold(toggle.native).perform
|
||||
active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_selector)
|
||||
page.driver.browser.action.release(toggle.native).perform
|
||||
|
||||
page.driver.browser.action.click(toggle.native).move_by(100, 100).perform
|
||||
focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_selector)
|
||||
|
||||
expect(default_background_color).not_to eq(hover_background_color)
|
||||
expect(hover_background_color).not_to eq(active_background_color)
|
||||
expect(default_background_color).not_to eq(active_background_color)
|
||||
|
||||
expect(default_foreground_color).not_to eq(hover_foreground_color)
|
||||
expect(hover_foreground_color).not_to eq(active_foreground_color)
|
||||
expect(default_foreground_color).not_to eq(active_foreground_color)
|
||||
|
||||
expect(focus_background_color).to eq(hover_background_color)
|
||||
expect(focus_foreground_color).to eq(hover_foreground_color)
|
||||
|
||||
expect(default_box_shadow).to eq('none')
|
||||
expect(hover_box_shadow).to eq('none')
|
||||
expect(active_box_shadow).not_to eq('none')
|
||||
expect(focus_box_shadow).not_to eq('none')
|
||||
end
|
||||
|
||||
it 'displays a mini pipeline graph' do
|
||||
expect(page).to have_selector('.mr-widget-pipeline-graph')
|
||||
it 'shows tooltip when hovered' do
|
||||
toggle.hover
|
||||
|
||||
expect(page).to have_selector('.tooltip')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'builds list menu' do
|
||||
let(:toggle) do
|
||||
find(dropdown_selector)
|
||||
first(dropdown_selector)
|
||||
end
|
||||
|
||||
context 'as json' do
|
||||
let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
|
||||
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
|
||||
before do
|
||||
toggle.click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
before do
|
||||
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
|
||||
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
|
||||
it 'pens when toggle is clicked' do
|
||||
expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
it 'closes when toggle is clicked again' do
|
||||
toggle.click
|
||||
|
||||
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
it 'closes when clicking somewhere else' do
|
||||
find('body').click
|
||||
|
||||
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
describe 'build list build item' do
|
||||
let(:build_item) do
|
||||
find('.mini-pipeline-graph-dropdown-item')
|
||||
first('.mini-pipeline-graph-dropdown-item')
|
||||
end
|
||||
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
|
||||
xit 'avoids repeated database queries' do
|
||||
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
|
||||
it 'visits the build page when clicked' do
|
||||
build_item.click
|
||||
find('.build-page')
|
||||
|
||||
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
|
||||
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
|
||||
|
||||
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
|
||||
|
||||
expect(before.count).to eq(after.count)
|
||||
expect(before.cached_count).to eq(after.cached_count)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build list toggle' do
|
||||
let(:toggle) do
|
||||
find(dropdown_toggle_selector)
|
||||
first(dropdown_toggle_selector)
|
||||
end
|
||||
|
||||
# Status icon button styles should update as described in
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
|
||||
it 'has unique styles for default, :hover, :active, and :focus states' do
|
||||
default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector)
|
||||
|
||||
toggle.hover
|
||||
hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector)
|
||||
|
||||
page.driver.browser.action.click_and_hold(toggle.native).perform
|
||||
active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector)
|
||||
page.driver.browser.action.release(toggle.native).perform
|
||||
|
||||
page.driver.browser.action.click(toggle.native).move_by(100, 100).perform
|
||||
focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector)
|
||||
|
||||
expect(default_background_color).not_to eq(hover_background_color)
|
||||
expect(hover_background_color).not_to eq(active_background_color)
|
||||
expect(default_background_color).not_to eq(active_background_color)
|
||||
|
||||
expect(default_foreground_color).not_to eq(hover_foreground_color)
|
||||
expect(hover_foreground_color).not_to eq(active_foreground_color)
|
||||
expect(default_foreground_color).not_to eq(active_foreground_color)
|
||||
|
||||
expect(focus_background_color).to eq(hover_background_color)
|
||||
expect(focus_foreground_color).to eq(hover_foreground_color)
|
||||
|
||||
expect(default_box_shadow).to eq('none')
|
||||
expect(hover_box_shadow).to eq('none')
|
||||
expect(active_box_shadow).not_to eq('none')
|
||||
expect(focus_box_shadow).not_to eq('none')
|
||||
expect(current_path).to eql(project_job_path(project, build))
|
||||
end
|
||||
|
||||
it 'shows tooltip when hovered' do
|
||||
toggle.hover
|
||||
build_item.hover
|
||||
|
||||
expect(page).to have_selector('.tooltip')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'builds list menu' do
|
||||
let(:toggle) do
|
||||
find(dropdown_toggle_selector)
|
||||
first(dropdown_toggle_selector)
|
||||
end
|
||||
|
||||
before do
|
||||
toggle.click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'pens when toggle is clicked' do
|
||||
expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
it 'closes when toggle is clicked again' do
|
||||
toggle.click
|
||||
|
||||
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
it 'closes when clicking somewhere else' do
|
||||
find('body').click
|
||||
|
||||
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
|
||||
end
|
||||
|
||||
describe 'build list build item' do
|
||||
let(:build_item) do
|
||||
find('.mini-pipeline-graph-dropdown-item')
|
||||
first('.mini-pipeline-graph-dropdown-item')
|
||||
end
|
||||
|
||||
it 'visits the build page when clicked' do
|
||||
build_item.click
|
||||
find('.build-page')
|
||||
|
||||
expect(current_path).to eql(project_job_path(project, build))
|
||||
end
|
||||
|
||||
it 'shows tooltip when hovered' do
|
||||
build_item.hover
|
||||
|
||||
expect(page).to have_selector('.tooltip')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ci_mini_pipeline_gl_dropdown disabled' do
|
||||
it_behaves_like "mini pipeline renders", false
|
||||
end
|
||||
|
||||
context 'with ci_mini_pipeline_gl_dropdown enabled' do
|
||||
it_behaves_like "mini pipeline renders", true
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -166,9 +149,9 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
|
|||
def get_toggle_colors(selector)
|
||||
find(selector)
|
||||
[
|
||||
evaluate_script("$('#{selector}:visible').css('background-color');"),
|
||||
evaluate_script("$('#{selector}:visible svg').css('fill');"),
|
||||
evaluate_script("$('#{selector}:visible').css('box-shadow');")
|
||||
evaluate_script("$('#{selector} button:visible').css('background-color');"),
|
||||
evaluate_script("$('#{selector} button:visible svg').css('fill');"),
|
||||
evaluate_script("$('#{selector} button:visible').css('box-shadow');")
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -519,75 +519,58 @@ RSpec.describe 'Pipelines', :js do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
|
||||
context 'mini pipeline graph' do
|
||||
context 'mini pipeline graph' do
|
||||
let!(:build) do
|
||||
create(:ci_build, :pending, pipeline: pipeline,
|
||||
stage: 'build',
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
|
||||
|
||||
before do
|
||||
visit_project_pipelines
|
||||
end
|
||||
|
||||
it 'renders a mini pipeline graph' do
|
||||
expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
|
||||
expect(page).to have_selector(dropdown_selector)
|
||||
end
|
||||
|
||||
context 'when clicking a stage badge' do
|
||||
it 'opens a dropdown' do
|
||||
find(dropdown_selector).click
|
||||
|
||||
expect(page).to have_link build.name
|
||||
end
|
||||
|
||||
it 'is possible to cancel pending build' do
|
||||
find(dropdown_selector).click
|
||||
find('.js-ci-action').click
|
||||
wait_for_requests
|
||||
|
||||
expect(build.reload).to be_canceled
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a failed pipeline' do
|
||||
let!(:build) do
|
||||
create(:ci_build, :pending, pipeline: pipeline,
|
||||
stage: 'build',
|
||||
name: 'build')
|
||||
create(:ci_build, :failed, pipeline: pipeline,
|
||||
stage: 'build',
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
|
||||
visit_project_pipelines
|
||||
end
|
||||
it 'displays the failure reason' do
|
||||
find(dropdown_selector).click
|
||||
|
||||
let_it_be(:dropdown_toggle_selector) do
|
||||
if ci_mini_pipeline_gl_dropdown_enabled
|
||||
'[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
|
||||
else
|
||||
'[data-testid="mini-pipeline-graph-dropdown-toggle"]'
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders a mini pipeline graph' do
|
||||
expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
|
||||
expect(page).to have_selector(dropdown_toggle_selector)
|
||||
end
|
||||
|
||||
context 'when clicking a stage badge' do
|
||||
it 'opens a dropdown' do
|
||||
find(dropdown_toggle_selector).click
|
||||
|
||||
expect(page).to have_link build.name
|
||||
end
|
||||
|
||||
it 'is possible to cancel pending build' do
|
||||
find(dropdown_toggle_selector).click
|
||||
find('.js-ci-action').click
|
||||
wait_for_requests
|
||||
|
||||
expect(build.reload).to be_canceled
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a failed pipeline' do
|
||||
let!(:build) do
|
||||
create(:ci_build, :failed, pipeline: pipeline,
|
||||
stage: 'build',
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
it 'displays the failure reason' do
|
||||
find(dropdown_toggle_selector).click
|
||||
|
||||
within('.js-builds-dropdown-list') do
|
||||
build_element = page.find('.mini-pipeline-graph-dropdown-item')
|
||||
expect(build_element['title']).to eq('build - failed - (unknown failure)')
|
||||
end
|
||||
within('.js-builds-dropdown-list') do
|
||||
build_element = page.find('.mini-pipeline-graph-dropdown-item')
|
||||
expect(build_element['title']).to eq('build - failed - (unknown failure)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ci_mini_pipeline_gl_dropdown disabled' do
|
||||
it_behaves_like "mini pipeline renders", false
|
||||
end
|
||||
|
||||
context 'with ci_mini_pipeline_gl_dropdown enabled' do
|
||||
it_behaves_like "mini pipeline renders", true
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
before do
|
||||
allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
|
||||
|
|
|
@ -1,38 +1,16 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "library/alpine",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1472,
|
||||
"digest": "sha256:7731472c3f2a25edbb9c085c78f42ec71259f2b83485aa60648276d408865839"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"8c59eb170e19b8c3768b8d06c91053b0debf4a6fa6a452df394145fe9b885ea5\",\"container_config\":{\"Hostname\":\"8c59eb170e19\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"/bin/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-10-22T02:19:24.499382102Z\",\"docker_version\":\"18.09.7\",\"id\":\"c5f1aab5bb88eaf1aa62bea08ea6654547d43fd4d15b1a476c77e705dd5385ba\",\"os\":\"linux\",\"parent\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"throwaway\":true}"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"created\":\"2020-10-22T02:19:24.33416307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:f17f65714f703db9012f00e5ec98d0b2541ff6147c2633f7ab9ba659d0c507f4 in / \"]}}"
|
||||
}
|
||||
],
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "XOTE:DZ4C:YBPJ:3O3L:YI4B:NYXU:T4VR:USH6:CXXN:SELU:CSCC:FVPE",
|
||||
"kty": "EC",
|
||||
"x": "cR1zye_3354mdbD7Dn-mtXNXvtPtmLlUVDa5vH6Lp74",
|
||||
"y": "rldUXSllLit6_2BW6AV8aqkwWJXHoYPG9OwkIBouwxQ"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "DYB2iB-XKIisqp5Q0OXFOBIOlBOuRV7pnZuKy0cxVB2Qj1VFRhWX4Tq336y0VMWbF6ma1he5A1E_Vk4jazrJ9g",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0xMS0yNFQyMjowMTo1MVoifQ"
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 2810825,
|
||||
"digest": "sha256:596ba82af5aaa3e2fd9d6f955b8b94f0744a2b60710e3c243ba3e4a467f051d1"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -32,8 +32,9 @@ describe('Configure Feature Flags Modal', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findGlModal = () => wrapper.find(GlModal);
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
const findPrimaryAction = () => findGlModal().props('actionPrimary');
|
||||
const findSecondaryAction = () => findGlModal().props('actionSecondary');
|
||||
const findProjectNameInput = () => wrapper.find('#project_name_verification');
|
||||
const findDangerGlAlert = () =>
|
||||
wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'danger');
|
||||
|
@ -42,18 +43,18 @@ describe('Configure Feature Flags Modal', () => {
|
|||
afterEach(() => wrapper.destroy());
|
||||
beforeEach(factory);
|
||||
|
||||
it('should have Primary and Cancel actions', () => {
|
||||
expect(findGlModal().props('actionCancel').text).toBe('Close');
|
||||
expect(findPrimaryAction().text).toBe('Regenerate instance ID');
|
||||
it('should have Primary and Secondary actions', () => {
|
||||
expect(findPrimaryAction().text).toBe('Close');
|
||||
expect(findSecondaryAction().text).toBe('Regenerate instance ID');
|
||||
});
|
||||
|
||||
it('should default disable the primary action', async () => {
|
||||
const [{ disabled }] = findPrimaryAction().attributes;
|
||||
it('should default disable the primary action', () => {
|
||||
const [{ disabled }] = findSecondaryAction().attributes;
|
||||
expect(disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should emit a `token` event when clicking on the Primary action', async () => {
|
||||
findGlModal().vm.$emit('primary', mockEvent);
|
||||
findGlModal().vm.$emit('secondary', mockEvent);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.emitted('token')).toEqual([[]]);
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
|
@ -112,10 +113,10 @@ describe('Configure Feature Flags Modal', () => {
|
|||
afterEach(() => wrapper.destroy());
|
||||
beforeEach(factory);
|
||||
|
||||
it('should enable the primary action', async () => {
|
||||
it('should enable the secondary action', async () => {
|
||||
findProjectNameInput().vm.$emit('input', provide.projectName);
|
||||
await wrapper.vm.$nextTick();
|
||||
const [{ disabled }] = findPrimaryAction().attributes;
|
||||
const [{ disabled }] = findSecondaryAction().attributes;
|
||||
expect(disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -124,8 +125,8 @@ describe('Configure Feature Flags Modal', () => {
|
|||
afterEach(() => wrapper.destroy());
|
||||
beforeEach(factory.bind(null, { canUserRotateToken: false }));
|
||||
|
||||
it('should not display the primary action', async () => {
|
||||
expect(findPrimaryAction()).toBe(null);
|
||||
it('should not display the primary action', () => {
|
||||
expect(findSecondaryAction()).toBe(null);
|
||||
});
|
||||
|
||||
it('should not display regenerating instance ID', async () => {
|
||||
|
|
|
@ -576,55 +576,95 @@ describe('GfmAutoComplete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Members.templateFunction', () => {
|
||||
it('should return html with avatarTag and username', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small></small> </li>');
|
||||
});
|
||||
describe('GfmAutoComplete.Members', () => {
|
||||
const member = {
|
||||
name: 'Marge Simpson',
|
||||
username: 'msimpson',
|
||||
search: 'MargeSimpson msimpson',
|
||||
};
|
||||
|
||||
it('should add icon if icon is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
|
||||
});
|
||||
describe('templateFunction', () => {
|
||||
it('should return html with avatarTag and username', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small></small> </li>');
|
||||
});
|
||||
|
||||
it('should add escaped title if title is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: 'MyGroup+',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
|
||||
});
|
||||
it('should add icon if icon is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
|
||||
});
|
||||
|
||||
it('should add user availability status if availabilityStatus is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
|
||||
}),
|
||||
).toBe(
|
||||
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
|
||||
);
|
||||
it('should add escaped title if title is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: 'MyGroup+',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '',
|
||||
}),
|
||||
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
|
||||
});
|
||||
|
||||
it('should add user availability status if availabilityStatus is set', () => {
|
||||
expect(
|
||||
GfmAutoComplete.Members.templateFunction({
|
||||
avatarTag: 'IMG',
|
||||
username: 'my-group',
|
||||
title: '',
|
||||
icon: '<i class="icon"/>',
|
||||
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
|
||||
}),
|
||||
).toBe(
|
||||
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
|
||||
);
|
||||
});
|
||||
|
||||
describe('nameOrUsernameStartsWith', () => {
|
||||
it.each`
|
||||
query | result
|
||||
${'mar'} | ${true}
|
||||
${'msi'} | ${true}
|
||||
${'margesimpson'} | ${true}
|
||||
${'msimpson'} | ${true}
|
||||
${'arge'} | ${false}
|
||||
${'rgesimp'} | ${false}
|
||||
${'maria'} | ${false}
|
||||
${'homer'} | ${false}
|
||||
`('returns $result for $query', ({ query, result }) => {
|
||||
expect(GfmAutoComplete.Members.nameOrUsernameStartsWith(member, query)).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nameOrUsernameIncludes', () => {
|
||||
it.each`
|
||||
query | result
|
||||
${'mar'} | ${true}
|
||||
${'msi'} | ${true}
|
||||
${'margesimpson'} | ${true}
|
||||
${'msimpson'} | ${true}
|
||||
${'arge'} | ${true}
|
||||
${'rgesimp'} | ${true}
|
||||
${'maria'} | ${false}
|
||||
${'homer'} | ${false}
|
||||
`('returns $result for $query', ({ query, result }) => {
|
||||
expect(GfmAutoComplete.Members.nameOrUsernameIncludes(member, query)).toBe(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { createMockClient } from 'mock-apollo-client';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import { STATUSES } from '~/import_entities/constants';
|
||||
import {
|
||||
clientTypenames,
|
||||
|
@ -18,6 +19,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import httpStatus from '~/lib/utils/http_status';
|
||||
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
|
||||
StatusPoller: jest.fn().mockImplementation(function mock() {
|
||||
this.startPolling = jest.fn();
|
||||
|
@ -287,6 +289,40 @@ describe('Bulk import resolvers', () => {
|
|||
|
||||
expect(results[0].status).toBe(STATUSES.NONE);
|
||||
});
|
||||
|
||||
it('shows default error message when server error is not provided', async () => {
|
||||
axiosMockAdapter
|
||||
.onPost(FAKE_ENDPOINTS.createBulkImport)
|
||||
.reply(httpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
client
|
||||
.mutate({
|
||||
mutation: importGroupMutation,
|
||||
variables: { sourceGroupId: GROUP_ID },
|
||||
})
|
||||
.catch(() => {});
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
|
||||
});
|
||||
|
||||
it('shows provided error message when error is included in backend response', async () => {
|
||||
const CUSTOM_MESSAGE = 'custom message';
|
||||
|
||||
axiosMockAdapter
|
||||
.onPost(FAKE_ENDPOINTS.createBulkImport)
|
||||
.reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });
|
||||
|
||||
client
|
||||
.mutate({
|
||||
mutation: importGroupMutation,
|
||||
variables: { sourceGroupId: GROUP_ID },
|
||||
})
|
||||
.catch(() => {});
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ exports[`packages_list_row renders 1`] = `
|
|||
data-qa-selector="package_row"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-py-3"
|
||||
class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5"
|
||||
>
|
||||
<!---->
|
||||
|
||||
|
|
|
@ -69,7 +69,8 @@ describe('Pipelines', () => {
|
|||
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
|
||||
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
|
||||
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
|
||||
const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
|
||||
const findStagesDropdownToggle = () =>
|
||||
wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle');
|
||||
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
|
||||
|
||||
const createComponent = (props = defaultProps) => {
|
||||
|
@ -642,7 +643,7 @@ describe('Pipelines', () => {
|
|||
// Mock init a polling cycle
|
||||
wrapper.vm.poll.options.notificationCallback(true);
|
||||
|
||||
findStagesDropdown().trigger('click');
|
||||
findStagesDropdownToggle().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -652,7 +653,9 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('stops polling & restarts polling', async () => {
|
||||
findStagesDropdown().trigger('click');
|
||||
findStagesDropdownToggle().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(cancelMock).not.toHaveBeenCalled();
|
||||
expect(stopMock).toHaveBeenCalled();
|
||||
|
|
|
@ -153,11 +153,10 @@ describe('Pipelines Table Row', () => {
|
|||
});
|
||||
|
||||
it('should render an icon for each stage', () => {
|
||||
expect(
|
||||
wrapper.findAll(
|
||||
'.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown-toggle"]',
|
||||
).length,
|
||||
).toEqual(pipeline.details.stages.length);
|
||||
const stages = wrapper.findAll(
|
||||
'.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown"]',
|
||||
);
|
||||
expect(stages).toHaveLength(pipeline.details.stages.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,47 +1,38 @@
|
|||
import 'bootstrap/js/dist/dropdown';
|
||||
import { GlDropdown } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import $ from 'jquery';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
import { stageReply } from './mock_data';
|
||||
|
||||
const dropdownPath = 'path.json';
|
||||
|
||||
describe('Pipelines stage component', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
let glFeatures;
|
||||
|
||||
const defaultProps = {
|
||||
stage: {
|
||||
status: {
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
title: 'success',
|
||||
},
|
||||
dropdown_path: 'path.json',
|
||||
},
|
||||
updateDropdown: false,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = mount(StageComponent, {
|
||||
attachTo: document.body,
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
stage: {
|
||||
status: {
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
title: 'success',
|
||||
},
|
||||
dropdown_path: dropdownPath,
|
||||
},
|
||||
updateDropdown: false,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
glFeatures = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -52,245 +43,142 @@ describe('Pipelines stage component', () => {
|
|||
mock.restore();
|
||||
});
|
||||
|
||||
describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => {
|
||||
const isDropdownOpen = () => wrapper.classes('show');
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
|
||||
const findDropdownMenu = () =>
|
||||
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
|
||||
const findCiActionBtn = () => wrapper.find('.js-ci-action');
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should render a dropdown with the status icon', () => {
|
||||
expect(wrapper.attributes('class')).toEqual('dropdown');
|
||||
expect(wrapper.find('svg').exists()).toBe(true);
|
||||
expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
|
||||
});
|
||||
const openStageDropdown = () => {
|
||||
findDropdownToggle().trigger('click');
|
||||
return new Promise((resolve) => {
|
||||
wrapper.vm.$root.$on('bv::dropdown::show', resolve);
|
||||
});
|
||||
};
|
||||
|
||||
describe('with successful request', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet('path.json').reply(200, stageReply);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should render the received data and emit `clickedDropdown` event', async () => {
|
||||
wrapper.find('button').trigger('click');
|
||||
|
||||
await axios.waitForAll();
|
||||
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
|
||||
stageReply.latest_statuses[0].name,
|
||||
);
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
||||
});
|
||||
});
|
||||
|
||||
it('when request fails should close the dropdown', async () => {
|
||||
mock.onGet('path.json').reply(500);
|
||||
describe('default appearance', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
wrapper.find({ ref: 'dropdown' }).trigger('click');
|
||||
|
||||
expect(isDropdownOpen()).toBe(true);
|
||||
|
||||
wrapper.find('button').trigger('click');
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(isDropdownOpen()).toBe(false);
|
||||
});
|
||||
|
||||
describe('update endpoint correctly', () => {
|
||||
beforeEach(() => {
|
||||
const copyStage = { ...stageReply };
|
||||
copyStage.latest_statuses[0].name = 'this is the updated content';
|
||||
mock.onGet('bar.json').reply(200, copyStage);
|
||||
createComponent({
|
||||
stage: {
|
||||
status: {
|
||||
group: 'running',
|
||||
icon: 'status_running',
|
||||
title: 'running',
|
||||
},
|
||||
dropdown_path: 'bar.json',
|
||||
},
|
||||
});
|
||||
return axios.waitForAll();
|
||||
});
|
||||
|
||||
it('should update the stage to request the new endpoint provided', async () => {
|
||||
wrapper.find('button').trigger('click');
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
|
||||
'this is the updated content',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelineActionRequestComplete', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet('path.json').reply(200, stageReply);
|
||||
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
|
||||
});
|
||||
|
||||
const clickCiAction = async () => {
|
||||
wrapper.find('button').trigger('click');
|
||||
await axios.waitForAll();
|
||||
|
||||
wrapper.find('.js-ci-action').trigger('click');
|
||||
await axios.waitForAll();
|
||||
};
|
||||
|
||||
describe('within pipeline table', () => {
|
||||
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
createComponent({ type: 'PIPELINES_TABLE' });
|
||||
|
||||
await clickCiAction();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('in MR widget', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn($.fn, 'dropdown');
|
||||
});
|
||||
|
||||
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
createComponent();
|
||||
|
||||
await clickCiAction();
|
||||
|
||||
expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
|
||||
});
|
||||
});
|
||||
it('should render a dropdown with the status icon', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
expect(findDropdownToggle().exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => {
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle');
|
||||
const findDropdownMenu = () =>
|
||||
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
|
||||
const findCiActionBtn = () => wrapper.find('.js-ci-action');
|
||||
|
||||
const openGlDropdown = () => {
|
||||
findDropdownToggle().trigger('click');
|
||||
return new Promise((resolve) => {
|
||||
wrapper.vm.$root.$on('bv::dropdown::show', resolve);
|
||||
});
|
||||
};
|
||||
|
||||
describe('when update dropdown is changed', () => {
|
||||
beforeEach(() => {
|
||||
glFeatures = { ciMiniPipelineGlDropdown: true };
|
||||
createComponent();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should render a dropdown with the status icon', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true);
|
||||
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with successful request', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet('path.json').reply(200, stageReply);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should render the received data and emit `clickedDropdown` event', async () => {
|
||||
await openGlDropdown();
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
||||
});
|
||||
});
|
||||
|
||||
it('when request fails should close the dropdown', async () => {
|
||||
mock.onGet('path.json').reply(500);
|
||||
|
||||
describe('when user opens dropdown and stage request is successful', () => {
|
||||
beforeEach(async () => {
|
||||
mock.onGet(dropdownPath).reply(200, stageReply);
|
||||
createComponent();
|
||||
|
||||
await openGlDropdown();
|
||||
await openStageDropdown();
|
||||
await axios.waitForAll();
|
||||
});
|
||||
|
||||
it('should render the received data and emit `clickedDropdown` event', async () => {
|
||||
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
|
||||
});
|
||||
|
||||
it('should refresh when updateDropdown is set to true', async () => {
|
||||
expect(mock.history.get).toHaveLength(1);
|
||||
|
||||
wrapper.setProps({ updateDropdown: true });
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(mock.history.get).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user opens dropdown and stage request fails', () => {
|
||||
beforeEach(async () => {
|
||||
mock.onGet(dropdownPath).reply(500);
|
||||
createComponent();
|
||||
|
||||
await openStageDropdown();
|
||||
await axios.waitForAll();
|
||||
});
|
||||
|
||||
it('should close the dropdown', () => {
|
||||
expect(findDropdown().classes('show')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update endpoint correctly', () => {
|
||||
beforeEach(async () => {
|
||||
const copyStage = { ...stageReply };
|
||||
copyStage.latest_statuses[0].name = 'this is the updated content';
|
||||
mock.onGet('bar.json').reply(200, copyStage);
|
||||
createComponent({
|
||||
stage: {
|
||||
status: {
|
||||
group: 'running',
|
||||
icon: 'status_running',
|
||||
title: 'running',
|
||||
},
|
||||
dropdown_path: 'bar.json',
|
||||
describe('update endpoint correctly', () => {
|
||||
beforeEach(async () => {
|
||||
const copyStage = { ...stageReply };
|
||||
copyStage.latest_statuses[0].name = 'this is the updated content';
|
||||
mock.onGet('bar.json').reply(200, copyStage);
|
||||
createComponent({
|
||||
stage: {
|
||||
status: {
|
||||
group: 'running',
|
||||
icon: 'status_running',
|
||||
title: 'running',
|
||||
},
|
||||
});
|
||||
await axios.waitForAll();
|
||||
dropdown_path: 'bar.json',
|
||||
},
|
||||
});
|
||||
await axios.waitForAll();
|
||||
});
|
||||
|
||||
it('should update the stage to request the new endpoint provided', async () => {
|
||||
await openStageDropdown();
|
||||
await axios.waitForAll();
|
||||
|
||||
expect(findDropdownMenu().text()).toContain('this is the updated content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelineActionRequestComplete', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(dropdownPath).reply(200, stageReply);
|
||||
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
|
||||
});
|
||||
|
||||
const clickCiAction = async () => {
|
||||
await openStageDropdown();
|
||||
await axios.waitForAll();
|
||||
|
||||
findCiActionBtn().trigger('click');
|
||||
await axios.waitForAll();
|
||||
};
|
||||
|
||||
describe('within pipeline table', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ type: 'PIPELINES_TABLE' });
|
||||
});
|
||||
|
||||
it('should update the stage to request the new endpoint provided', async () => {
|
||||
await openGlDropdown();
|
||||
await axios.waitForAll();
|
||||
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
await clickCiAction();
|
||||
|
||||
expect(findDropdownMenu().text()).toContain('this is the updated content');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelineActionRequestComplete', () => {
|
||||
describe('in MR widget', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet('path.json').reply(200, stageReply);
|
||||
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
const clickCiAction = async () => {
|
||||
await openGlDropdown();
|
||||
await axios.waitForAll();
|
||||
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
const hidden = jest.fn();
|
||||
|
||||
findCiActionBtn().trigger('click');
|
||||
await axios.waitForAll();
|
||||
};
|
||||
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
|
||||
|
||||
describe('within pipeline table', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ type: 'PIPELINES_TABLE' });
|
||||
});
|
||||
expect(hidden).toHaveBeenCalledTimes(0);
|
||||
|
||||
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
await clickCiAction();
|
||||
await clickCiAction();
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('in MR widget', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn($.fn, 'dropdown');
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
const hidden = jest.fn();
|
||||
|
||||
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
|
||||
|
||||
expect(hidden).toHaveBeenCalledTimes(0);
|
||||
|
||||
await clickCiAction();
|
||||
|
||||
expect(hidden).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(hidden).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import Component from '~/registry/explorer/components/list_page/registry_header.
|
|||
import {
|
||||
CONTAINER_REGISTRY_TITLE,
|
||||
LIST_INTRO_TEXT,
|
||||
EXPIRATION_POLICY_DISABLED_MESSAGE,
|
||||
EXPIRATION_POLICY_DISABLED_TEXT,
|
||||
} from '~/registry/explorer/constants';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
|
@ -132,41 +131,5 @@ describe('registry_header', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expiration policy info message', () => {
|
||||
describe('when there are images', () => {
|
||||
describe('when expiration policy is disabled', () => {
|
||||
beforeEach(() => {
|
||||
return mountComponent({
|
||||
expirationPolicy: { enabled: false },
|
||||
expirationPolicyHelpPagePath: 'foo',
|
||||
imagesCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('the prop is correctly bound', () => {
|
||||
expect(findTitleArea().props('infoMessages')).toEqual([
|
||||
{ text: LIST_INTRO_TEXT, link: '' },
|
||||
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: 'foo' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
desc | props
|
||||
${'when there are no images'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 0 }}
|
||||
${'when expiration policy is enabled'} | ${{ expirationPolicy: { enabled: true }, imagesCount: 1 }}
|
||||
${'when the expiration policy is completely disabled'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 1, hideExpirationPolicyData: true }}
|
||||
`('$desc', ({ props }) => {
|
||||
it('message does not exist', () => {
|
||||
mountComponent(props);
|
||||
|
||||
expect(findTitleArea().props('infoMessages')).toEqual([
|
||||
{ text: LIST_INTRO_TEXT, link: '' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::ProjectRepositoryStorageMove do
|
||||
RSpec.describe API::Entities::Projects::RepositoryStorageMove do
|
||||
describe '#as_json' do
|
||||
subject { entity.as_json }
|
||||
|
|
@ -68,5 +68,11 @@ RSpec.describe BulkImports::Common::Transformers::ProhibitedAttributesTransforme
|
|||
|
||||
expect(transformed_hash).to eq(expected_hash)
|
||||
end
|
||||
|
||||
context 'when there is no data to transform' do
|
||||
it 'returns' do
|
||||
expect(subject.transform(nil, nil)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
|
||||
RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do
|
||||
describe '#transform' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
@ -12,7 +12,6 @@ RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
|
|||
|
||||
let(:hash) do
|
||||
{
|
||||
'name' => 'thumbs up',
|
||||
'user' => {
|
||||
'public_email' => email
|
||||
}
|
||||
|
@ -44,5 +43,27 @@ RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
|
|||
|
||||
include_examples 'sets user_id and removes user key'
|
||||
end
|
||||
|
||||
context 'when there is no data to transform' do
|
||||
it 'returns' do
|
||||
expect(subject.transform(nil, nil)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom reference is provided' do
|
||||
it 'updates provided reference' do
|
||||
hash = {
|
||||
'author' => {
|
||||
'public_email' => user.email
|
||||
}
|
||||
}
|
||||
|
||||
transformer = described_class.new(reference: 'author')
|
||||
result = transformer.transform(context, hash)
|
||||
|
||||
expect(result['author']).to be_nil
|
||||
expect(result['author_id']).to eq(user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -117,4 +117,27 @@ RSpec.describe BulkImports::Pipeline do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#transformers' do
|
||||
before do
|
||||
klass = Class.new do
|
||||
include BulkImports::Pipeline
|
||||
|
||||
transformer BulkImports::Transformer
|
||||
|
||||
def transform; end
|
||||
end
|
||||
|
||||
stub_const('BulkImports::TransformersPipeline', klass)
|
||||
end
|
||||
|
||||
it 'has instance transform method first to run' do
|
||||
transformer = double
|
||||
allow(BulkImports::Transformer).to receive(:new).and_return(transformer)
|
||||
|
||||
pipeline = BulkImports::TransformersPipeline.new(nil)
|
||||
|
||||
expect(pipeline.send(:transformers)).to eq([pipeline, transformer])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,24 +29,32 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.find_or_initialize_by_file_name' do
|
||||
subject { DependencyProxy::Manifest.find_or_initialize_by_file_name(file_name) }
|
||||
describe '.find_or_initialize_by_file_name_or_digest' do
|
||||
let_it_be(:file_name) { 'foo' }
|
||||
let_it_be(:digest) { 'bar' }
|
||||
|
||||
subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
|
||||
|
||||
context 'no manifest exists' do
|
||||
let_it_be(:file_name) { 'foo' }
|
||||
|
||||
it 'initializes a manifest' do
|
||||
expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name)
|
||||
expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'manifest exists' do
|
||||
context 'manifest exists and matches file_name' do
|
||||
let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) }
|
||||
let_it_be(:file_name) { dependency_proxy_manifest.file_name }
|
||||
|
||||
it { is_expected.to eq(dependency_proxy_manifest) }
|
||||
end
|
||||
|
||||
context 'manifest exists and matches digest' do
|
||||
let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) }
|
||||
let_it_be(:digest) { dependency_proxy_manifest.digest }
|
||||
|
||||
it { is_expected.to eq(dependency_proxy_manifest) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1045,6 +1045,29 @@ RSpec.describe Group do
|
|||
include(group_user.id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'distinct user ids' do
|
||||
let_it_be(:subgroup) { create(:group, :nested) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:shared_with_group) { create(:group) }
|
||||
let_it_be(:other_subgroup_user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:group_group_link, shared_group: subgroup, shared_with_group: shared_with_group)
|
||||
subgroup.add_maintainer(other_subgroup_user)
|
||||
|
||||
# `user` is added as a direct member of the parent group, the subgroup
|
||||
# and another group shared with the subgroup.
|
||||
subgroup.parent.add_maintainer(user)
|
||||
subgroup.add_developer(user)
|
||||
shared_with_group.add_guest(user)
|
||||
end
|
||||
|
||||
it 'returns only distinct user ids of users for which to refresh authorizations' do
|
||||
expect(subgroup.user_ids_for_project_authorizations).to(
|
||||
contain_exactly(user.id, other_subgroup_user.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_two_factor_requirement' do
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do
|
|||
let(:container) { project }
|
||||
let(:repository_storage_factory_key) { :project_repository_storage_move }
|
||||
let(:error_key) { :project }
|
||||
let(:repository_storage_worker) { ProjectUpdateRepositoryStorageWorker }
|
||||
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
|
||||
end
|
||||
|
||||
describe 'state transitions' do
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::RepositoryStorageMove, type: :model do
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
|
||||
it_behaves_like 'handles repository moves' do
|
||||
let(:container) { project }
|
||||
let(:repository_storage_factory_key) { :project_repository_storage_move }
|
||||
let(:error_key) { :project }
|
||||
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
|
||||
end
|
||||
|
||||
describe 'state transitions' do
|
||||
let(:storage) { 'test_second_storage' }
|
||||
|
||||
before do
|
||||
stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' })
|
||||
end
|
||||
|
||||
context 'when started' do
|
||||
subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) }
|
||||
|
||||
context 'and transits to replicated' do
|
||||
it 'sets the repository storage and marks the container as writable' do
|
||||
storage_move.finish_replication!
|
||||
|
||||
expect(project.repository_storage).to eq(storage)
|
||||
expect(project).not_to be_repository_read_only
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,6 +7,6 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
|
|||
let_it_be(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
|
||||
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
|
||||
let(:repository_storage_move_factory) { :project_repository_storage_move }
|
||||
let(:bulk_worker_klass) { ProjectScheduleBulkRepositoryShardMovesWorker }
|
||||
let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker }
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue