Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
abbedc2027
commit
4ac9f1b8ea
|
@ -37,6 +37,7 @@ import initBroadcastNotifications from './broadcast_notification';
|
|||
import { initTopNav } from './nav';
|
||||
|
||||
import 'ee_else_ce/main_ee';
|
||||
import 'jh_else_ce/main_jh';
|
||||
|
||||
applyGitLabUIConfig();
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// This is an empty file to satisfy jh_else_ce import for the JH main entry point
|
|
@ -258,6 +258,7 @@ export function mountSidebarLabels() {
|
|||
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
|
||||
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
|
||||
variant: DropdownVariant.Sidebar,
|
||||
canUpdate: parseBoolean(el.dataset.canEdit),
|
||||
},
|
||||
render: (createElement) => createElement(SidebarLabels),
|
||||
});
|
||||
|
|
|
@ -60,7 +60,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']),
|
||||
...mapActions(['toggleDropdownContentsCreateView']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -83,7 +83,7 @@ export default {
|
|||
size="small"
|
||||
class="js-btn-back dropdown-header-button p-0"
|
||||
icon="arrow-left"
|
||||
@click="toggleDropdownContentsCreateView"
|
||||
@click.stop="toggleDropdownContentsCreateView"
|
||||
/>
|
||||
<span class="flex-grow-1">{{ dropdownTitle }}</span>
|
||||
<gl-button
|
||||
|
@ -92,7 +92,7 @@ export default {
|
|||
size="small"
|
||||
class="dropdown-header-button gl-p-0!"
|
||||
icon="close"
|
||||
@click="toggleDropdownContents"
|
||||
@click="$emit('closeDropdown')"
|
||||
/>
|
||||
</div>
|
||||
<component
|
||||
|
@ -103,7 +103,7 @@ export default {
|
|||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
@hideCreateView="toggleDropdownContentsCreateView"
|
||||
@closeDropdown="$emit('closeDropdown', $event)"
|
||||
@setLabels="$emit('setLabels', $event)"
|
||||
@toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
|
||||
import produce from 'immer';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import createLabelMutation from './graphql/create_label.mutation.graphql';
|
||||
import projectLabelsQuery from './graphql/project_labels.query.graphql';
|
||||
|
||||
const errorMessage = __('Error creating label.');
|
||||
|
||||
|
@ -47,6 +49,25 @@ export default {
|
|||
handleColorClick(color) {
|
||||
this.selectedColor = this.getColorCode(color);
|
||||
},
|
||||
updateLabelsInCache(store, label) {
|
||||
const sourceData = store.readQuery({
|
||||
query: projectLabelsQuery,
|
||||
variables: { fullPath: this.projectPath, searchTerm: '' },
|
||||
});
|
||||
|
||||
const collator = new Intl.Collator('en');
|
||||
const data = produce(sourceData, (draftData) => {
|
||||
const { nodes } = draftData.workspace.labels;
|
||||
nodes.push(label);
|
||||
nodes.sort((a, b) => collator.compare(a.title, b.title));
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: projectLabelsQuery,
|
||||
variables: { fullPath: this.projectPath, searchTerm: '' },
|
||||
data,
|
||||
});
|
||||
},
|
||||
async createLabel() {
|
||||
this.labelCreateInProgress = true;
|
||||
try {
|
||||
|
@ -59,6 +80,14 @@ export default {
|
|||
color: this.selectedColor,
|
||||
projectPath: this.projectPath,
|
||||
},
|
||||
update: (
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
labelCreate: { label },
|
||||
},
|
||||
},
|
||||
) => this.updateLabelsInCache(store, label),
|
||||
});
|
||||
if (labelCreate.errors.length) {
|
||||
createFlash({ message: errorMessage });
|
||||
|
|
|
@ -112,7 +112,7 @@ export default {
|
|||
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$emit('closeDropdown', this.localSelectedLabels);
|
||||
this.$emit('setLabels', this.localSelectedLabels);
|
||||
this.debouncedSearchKeyUpdate.cancel();
|
||||
},
|
||||
methods: {
|
||||
|
@ -166,7 +166,7 @@ export default {
|
|||
this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
|
||||
this.searchKey = '';
|
||||
} else if (e.keyCode === ESC_KEY_CODE) {
|
||||
this.$emit('closeDropdown', this.localSelectedLabels);
|
||||
this.$emit('setLabels', this.localSelectedLabels);
|
||||
}
|
||||
|
||||
if (e.keyCode !== ESC_KEY_CODE) {
|
||||
|
@ -180,7 +180,7 @@ export default {
|
|||
handleLabelClick(label) {
|
||||
this.updateSelectedLabels(label);
|
||||
if (!this.allowMultiselect) {
|
||||
this.$emit('closeDropdown', this.localSelectedLabels);
|
||||
this.$emit('setLabels', this.localSelectedLabels);
|
||||
}
|
||||
},
|
||||
setSearchKey(value) {
|
||||
|
@ -240,7 +240,7 @@ export default {
|
|||
<gl-link
|
||||
class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
|
||||
data-testid="create-label-button"
|
||||
@click="$emit('toggleDropdownContentsCreateView')"
|
||||
@click.stop="$emit('toggleDropdownContentsCreateView')"
|
||||
>
|
||||
{{ footerCreateLabelTitle }}
|
||||
</gl-link>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
labelsSelectInProgress: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['allowLabelEdit', 'labelsFetchInProgress']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDropdownContents']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="title hide-collapsed gl-mb-3">
|
||||
{{ __('Labels') }}
|
||||
<template v-if="allowLabelEdit">
|
||||
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
|
||||
data-qa-selector="labels_edit_button"
|
||||
@click="toggleDropdownContents"
|
||||
>{{ __('Edit') }}</gl-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -6,9 +6,7 @@ mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPa
|
|||
id
|
||||
color
|
||||
description
|
||||
descriptionHtml
|
||||
title
|
||||
textColor
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { isInViewport } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import { DropdownVariant } from './constants';
|
||||
import DropdownButton from './dropdown_button.vue';
|
||||
import DropdownContents from './dropdown_contents.vue';
|
||||
import DropdownTitle from './dropdown_title.vue';
|
||||
import DropdownValue from './dropdown_value.vue';
|
||||
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
|
||||
import issueLabelsQuery from './graphql/issue_labels.query.graphql';
|
||||
|
@ -19,11 +17,11 @@ Vue.use(Vuex);
|
|||
export default {
|
||||
store: new Vuex.Store(labelsSelectModule()),
|
||||
components: {
|
||||
DropdownTitle,
|
||||
DropdownValue,
|
||||
DropdownButton,
|
||||
DropdownContents,
|
||||
DropdownValueCollapsed,
|
||||
SidebarEditableItem,
|
||||
},
|
||||
inject: ['iid', 'projectPath'],
|
||||
props: {
|
||||
|
@ -139,15 +137,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['showDropdownButton', 'showDropdownContents']),
|
||||
...mapState(['showDropdownContents']),
|
||||
...mapGetters([
|
||||
'isDropdownVariantSidebar',
|
||||
'isDropdownVariantStandalone',
|
||||
'isDropdownVariantEmbedded',
|
||||
]),
|
||||
dropdownButtonVisible() {
|
||||
return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedLabels(selectedLabels) {
|
||||
|
@ -182,99 +177,20 @@ export default {
|
|||
footerCreateLabelTitle: this.footerCreateLabelTitle,
|
||||
footerManageLabelTitle: this.footerManageLabelTitle,
|
||||
});
|
||||
|
||||
this.$store.subscribeAction({
|
||||
after: this.handleVuexActionDispatch,
|
||||
});
|
||||
|
||||
document.addEventListener('mousedown', this.handleDocumentMousedown);
|
||||
document.addEventListener('click', this.handleDocumentClick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('mousedown', this.handleDocumentMousedown);
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setInitialState', 'toggleDropdownContents']),
|
||||
/**
|
||||
* This method stores a mousedown event's target.
|
||||
* Required by the click listener because the click
|
||||
* event itself has no reference to this element.
|
||||
*/
|
||||
handleDocumentMousedown({ target }) {
|
||||
this.mousedownTarget = target;
|
||||
},
|
||||
/**
|
||||
* This method listens for document-wide click event
|
||||
* and toggle dropdown if user clicks anywhere outside
|
||||
* the dropdown while dropdown is visible.
|
||||
*/
|
||||
handleDocumentClick({ target }) {
|
||||
// We also perform the toggle exception check for the
|
||||
// last mousedown event's target to avoid hiding the
|
||||
// box when the mousedown happened inside the box and
|
||||
// only the mouseup did not.
|
||||
if (
|
||||
this.showDropdownContents &&
|
||||
!this.preventDropdownToggleOnClick(target) &&
|
||||
!this.preventDropdownToggleOnClick(this.mousedownTarget)
|
||||
) {
|
||||
this.toggleDropdownContents();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This method checks whether a given click target
|
||||
* should prevent the dropdown from being toggled.
|
||||
*/
|
||||
preventDropdownToggleOnClick(target) {
|
||||
// This approach of element detection is needed
|
||||
// as the dropdown wrapper is not using `GlDropdown` as
|
||||
// it will also require us to use `BDropdownForm`
|
||||
// which is yet to be implemented in GitLab UI.
|
||||
const hasExceptionClass = [
|
||||
'js-dropdown-button',
|
||||
'js-btn-cancel-create',
|
||||
'js-sidebar-dropdown-toggle',
|
||||
].some(
|
||||
(className) =>
|
||||
target?.classList.contains(className) ||
|
||||
target?.parentElement?.classList.contains(className),
|
||||
);
|
||||
|
||||
const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
|
||||
(className) => $(target).parents(className).length,
|
||||
);
|
||||
|
||||
const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target);
|
||||
|
||||
const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target);
|
||||
|
||||
return (
|
||||
hasExceptionClass ||
|
||||
hasExceptionParent ||
|
||||
isInDropdownButtonCollapsed ||
|
||||
isInDropdownContents
|
||||
);
|
||||
},
|
||||
...mapActions(['setInitialState']),
|
||||
handleDropdownClose(labels) {
|
||||
// Only emit label updates if there are any labels to update
|
||||
// on UI.
|
||||
if (this.showDropdownContents) {
|
||||
this.toggleDropdownContents();
|
||||
}
|
||||
if (labels.length) this.$emit('updateSelectedLabels', labels);
|
||||
this.$emit('onDropdownClose');
|
||||
},
|
||||
collapseDropdown() {
|
||||
this.$refs.editable.collapse();
|
||||
},
|
||||
handleCollapsedValueClick() {
|
||||
this.$emit('toggleCollapse');
|
||||
},
|
||||
setContentIsOnViewport(showDropdownContents) {
|
||||
if (!showDropdownContents) {
|
||||
this.contentIsOnViewport = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setContentIsOnViewport() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.dropdownContents) {
|
||||
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
|
||||
|
@ -299,48 +215,55 @@ export default {
|
|||
:labels="issueLabels"
|
||||
@onValueClick="handleCollapsedValueClick"
|
||||
/>
|
||||
<dropdown-title
|
||||
:allow-label-edit="allowLabelEdit"
|
||||
:labels-select-in-progress="labelsSelectInProgress"
|
||||
/>
|
||||
<dropdown-value
|
||||
:disable-labels="labelsSelectInProgress"
|
||||
:selected-labels="issueLabels"
|
||||
:allow-label-remove="allowLabelRemove"
|
||||
:allow-scoped-labels="allowScopedLabels"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-filter-param="labelsFilterParam"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
<sidebar-editable-item
|
||||
ref="editable"
|
||||
:title="__('Labels')"
|
||||
:loading="labelsSelectInProgress"
|
||||
@open="setContentIsOnViewport"
|
||||
@close="contentIsOnViewport = true"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
|
||||
<dropdown-contents
|
||||
v-if="dropdownButtonVisible && showDropdownContents"
|
||||
ref="dropdownContents"
|
||||
:allow-multiselect="allowMultiselect"
|
||||
:labels-list-title="labelsListTitle"
|
||||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
:render-on-top="!contentIsOnViewport"
|
||||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="selectedLabels"
|
||||
@closeDropdown="handleDropdownClose"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
|
||||
<dropdown-button v-show="dropdownButtonVisible" />
|
||||
<dropdown-contents
|
||||
v-if="dropdownButtonVisible && showDropdownContents"
|
||||
ref="dropdownContents"
|
||||
:allow-multiselect="allowMultiselect"
|
||||
:labels-list-title="labelsListTitle"
|
||||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
:render-on-top="!contentIsOnViewport"
|
||||
:selected-labels="selectedLabels"
|
||||
@closeDropdown="handleDropdownClose"
|
||||
/>
|
||||
<template #collapsed>
|
||||
<dropdown-value
|
||||
:disable-labels="labelsSelectInProgress"
|
||||
:selected-labels="issueLabels"
|
||||
:allow-label-remove="allowLabelRemove"
|
||||
:allow-scoped-labels="allowScopedLabels"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-filter-param="labelsFilterParam"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
</template>
|
||||
<template #default="{ edit }">
|
||||
<dropdown-value
|
||||
:disable-labels="labelsSelectInProgress"
|
||||
:selected-labels="issueLabels"
|
||||
:allow-label-remove="allowLabelRemove"
|
||||
:allow-scoped-labels="allowScopedLabels"
|
||||
:labels-filter-base-path="labelsFilterBasePath"
|
||||
:labels-filter-param="labelsFilterParam"
|
||||
class="gl-mb-2"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<dropdown-button />
|
||||
<dropdown-contents
|
||||
v-if="edit"
|
||||
ref="dropdownContents"
|
||||
:allow-multiselect="allowMultiselect"
|
||||
:labels-list-title="labelsListTitle"
|
||||
:footer-create-label-title="footerCreateLabelTitle"
|
||||
:footer-manage-label-title="footerManageLabelTitle"
|
||||
:render-on-top="!contentIsOnViewport"
|
||||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="selectedLabels"
|
||||
@closeDropdown="collapseDropdown"
|
||||
@setLabels="handleDropdownClose"
|
||||
/>
|
||||
</template>
|
||||
</sidebar-editable-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,8 @@ module Packages
|
|||
DEFAULT_PACKAGE_FILES_COUNT = 20
|
||||
MAX_PACKAGE_FILES_COUNT = 1000
|
||||
|
||||
delegate :most_recent!, to: :execute
|
||||
|
||||
def initialize(project, channel, params = {})
|
||||
@project = project
|
||||
@channel = channel
|
||||
|
|
|
@ -77,6 +77,10 @@ class Packages::PackageFile < ApplicationRecord
|
|||
.where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference })
|
||||
end
|
||||
|
||||
def self.most_recent!
|
||||
recent.first!
|
||||
end
|
||||
|
||||
mount_file_store_uploader Packages::PackageFileUploader
|
||||
|
||||
update_project_statistics project_statistics_name: :packages_size
|
||||
|
|
|
@ -1000,7 +1000,11 @@ class User < ApplicationRecord
|
|||
|
||||
# Returns the groups a user is a member of, either directly or through a parent group
|
||||
def membership_groups
|
||||
Gitlab::ObjectHierarchy.new(groups).base_and_descendants
|
||||
if Feature.enabled?(:linear_user_membership_groups, self, default_enabled: :yaml)
|
||||
groups.self_and_descendants
|
||||
else
|
||||
Gitlab::ObjectHierarchy.new(groups).base_and_descendants
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a relation of groups the user has access to, including their parent
|
||||
|
|
|
@ -431,22 +431,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
private
|
||||
|
||||
def integrations_anchor_data
|
||||
experiment(:repo_integrations_link, project: project) do |e|
|
||||
e.exclude! unless can?(current_user, :admin_project, project)
|
||||
return unless can?(current_user, :admin_project, project)
|
||||
|
||||
e.use {} # nil control
|
||||
e.try do
|
||||
label = statistic_icon('settings') + _('Configure Integrations')
|
||||
AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil, {
|
||||
'track-event': 'click',
|
||||
'track-experiment': e.name
|
||||
})
|
||||
end
|
||||
|
||||
e.run # call run so the return value will be the AnchorData (or nil)
|
||||
|
||||
e.track(:view, value: project.id) # track an event for the view, with project id
|
||||
end
|
||||
label = statistic_icon('settings') + _('Configure Integrations')
|
||||
AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil)
|
||||
end
|
||||
|
||||
def cicd_missing?
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referer), class: link_classes + 'btn gl-button btn-default btn-icon',
|
||||
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
= sprite_icon('error')
|
||||
- verified_gpg_keys = @user.gpg_keys.select(&:verified?)
|
||||
- if verified_gpg_keys.any?
|
||||
= link_to user_gpg_keys_path,
|
||||
class: link_classes + 'btn btn-default btn-md gl-button btn-icon has-tooltip',
|
||||
title: n_('View public GPG key', 'View public GPG keys', verified_gpg_keys.length),
|
||||
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
= sprite_icon('key', css_class: 'gl-button-icon gl-icon')
|
||||
- if can?(current_user, :read_user_profile, @user)
|
||||
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn gl-button btn-default btn-icon has-tooltip',
|
||||
title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||
|
|
|
@ -22,12 +22,7 @@ module Packages
|
|||
return unless package_file && user
|
||||
|
||||
::Packages::Debian::ProcessChangesService.new(package_file, user).execute
|
||||
rescue ArgumentError,
|
||||
Packages::Debian::ExtractChangesMetadataService::ExtractionError,
|
||||
Packages::Debian::ExtractDebMetadataService::CommandFailedError,
|
||||
Packages::Debian::ExtractMetadataService::ExtractionError,
|
||||
Packages::Debian::ParseDebian822Service::InvalidDebian822Error,
|
||||
ActiveRecord::RecordNotFound => e
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id)
|
||||
package_file.destroy!
|
||||
end
|
||||
|
|
|
@ -20,9 +20,7 @@ module Packages
|
|||
|
||||
::Packages::Helm::ProcessFileService.new(channel, package_file).execute
|
||||
|
||||
rescue ::Packages::Helm::ExtractFileMetadataService::ExtractionError,
|
||||
::Packages::Helm::ProcessFileService::ExtractionError,
|
||||
::ActiveModel::ValidationError => e
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id)
|
||||
package_file.package.update_column(:status, :error)
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: repo_integrations_link
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54652/
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285154
|
||||
milestone: '13.10'
|
||||
type: experiment
|
||||
group: group::adoption
|
||||
name: linear_user_membership_groups
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68842
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339432
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: false
|
|
@ -1,131 +0,0 @@
|
|||
/* eslint-disable max-classes-per-file, no-underscore-dangle */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest);
|
||||
|
||||
// If we force a recompile immediately, the page reload doesn't seem to work.
|
||||
// Five seconds seem to work fine and the user can read the message
|
||||
const TIMEOUT = 5000;
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
class NoopCompiler {
|
||||
constructor() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
filterEntryPoints(entryPoints) {
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
logStatus() {}
|
||||
|
||||
setupMiddleware() {}
|
||||
}
|
||||
/* eslint-enable class-methods-use-this */
|
||||
|
||||
class IncrementalWebpackCompiler {
|
||||
constructor(historyFilePath) {
|
||||
this.enabled = true;
|
||||
this.history = {};
|
||||
this.compiledEntryPoints = new Set([
|
||||
// Login page
|
||||
'pages.sessions.new',
|
||||
// Explore page
|
||||
'pages.root',
|
||||
]);
|
||||
this.historyFilePath = historyFilePath;
|
||||
this._loadFromHistory();
|
||||
}
|
||||
|
||||
filterEntryPoints(entrypoints) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(entrypoints).map(([key, val]) => {
|
||||
if (this.compiledEntryPoints.has(key)) {
|
||||
return [key, val];
|
||||
}
|
||||
return [key, ['./webpack_non_compiled_placeholder.js']];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
logStatus(totalCount) {
|
||||
const current = this.compiledEntryPoints.size;
|
||||
log(`Currently compiling route entrypoints: ${current} of ${totalCount}`);
|
||||
}
|
||||
|
||||
setupMiddleware(app, server) {
|
||||
app.use((req, res, next) => {
|
||||
const fileName = path.basename(req.url);
|
||||
|
||||
/**
|
||||
* We are only interested in files that have a name like `pages.foo.bar.chunk.js`
|
||||
* because those are the ones corresponding to our entry points.
|
||||
*
|
||||
* This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js"
|
||||
*/
|
||||
if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) {
|
||||
const chunk = fileName.replace(/\.chunk\.js$/, '');
|
||||
|
||||
this._addToHistory(chunk);
|
||||
|
||||
if (!this.compiledEntryPoints.has(chunk)) {
|
||||
log(`First time we are seeing ${chunk}. Adding to compilation.`);
|
||||
|
||||
this.compiledEntryPoints.add(chunk);
|
||||
|
||||
setTimeout(() => {
|
||||
server.middleware.invalidate(() => {
|
||||
if (server.sockets) {
|
||||
server.sockWrite(server.sockets, 'content-changed');
|
||||
}
|
||||
});
|
||||
}, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// private methods
|
||||
|
||||
_addToHistory(chunk) {
|
||||
if (!this.history[chunk]) {
|
||||
this.history[chunk] = { lastVisit: null, count: 0 };
|
||||
}
|
||||
this.history[chunk].lastVisit = Date.now();
|
||||
this.history[chunk].count += 1;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(this.historyFilePath, JSON.stringify(this.history), 'utf8');
|
||||
} catch (e) {
|
||||
log('Warning – Could not write to history', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
_loadFromHistory() {
|
||||
try {
|
||||
this.history = JSON.parse(fs.readFileSync(this.historyFilePath, 'utf8'));
|
||||
const entryPoints = Object.keys(this.history);
|
||||
log(`Successfully loaded history containing ${entryPoints.length} entry points`);
|
||||
/*
|
||||
TODO: Let's ask a few folks to give us their history file after a milestone of usage
|
||||
Then we can make smarter decisions on when to throw out rather than rendering everything
|
||||
Something like top 20/30/40 entries visited in the last 7/10/15 days might be sufficient
|
||||
*/
|
||||
this.compiledEntryPoints = new Set([...this.compiledEntryPoints, ...entryPoints]);
|
||||
} catch (e) {
|
||||
log(`No history found...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (enabled, historyFilePath) => {
|
||||
log(`Status – ${enabled ? 'enabled' : 'disabled'}`);
|
||||
|
||||
if (enabled) {
|
||||
return new IncrementalWebpackCompiler(historyFilePath);
|
||||
}
|
||||
return new NoopCompiler();
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
const path = require('path');
|
||||
const { History, HistoryWithTTL } = require('./history');
|
||||
const log = require('./log');
|
||||
|
||||
const onRequestEntryPoint = (app, callback) => {
|
||||
app.use((req, res, next) => {
|
||||
const fileName = path.basename(req.url);
|
||||
|
||||
/**
|
||||
* We are only interested in files that have a name like `pages.foo.bar.chunk.js`
|
||||
* because those are the ones corresponding to our entry points.
|
||||
*
|
||||
* This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js"
|
||||
*/
|
||||
if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) {
|
||||
const entryPoint = fileName.replace(/\.chunk\.js$/, '');
|
||||
callback(entryPoint);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The NoopCompiler does nothing, following the null object pattern.
|
||||
*/
|
||||
class NoopCompiler {
|
||||
constructor() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
filterEntryPoints(entryPoints) {
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
logStatus() {}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setupMiddleware() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The HistoryOnlyCompiler only records which entry points have been requested.
|
||||
* This is so that if the user disables incremental compilation, history is
|
||||
* still recorded. If they later enable incremental compilation, that history
|
||||
* can be used.
|
||||
*/
|
||||
class HistoryOnlyCompiler extends NoopCompiler {
|
||||
constructor(historyFilePath) {
|
||||
super();
|
||||
this.history = new History(historyFilePath);
|
||||
}
|
||||
|
||||
setupMiddleware(app) {
|
||||
onRequestEntryPoint(app, (entryPoint) => {
|
||||
this.history.onRequestEntryPoint(entryPoint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If we force a recompile immediately, the page reload doesn't seem to work.
|
||||
// Five seconds seem to work fine and the user can read the message
|
||||
const TIMEOUT = 5000;
|
||||
|
||||
/**
|
||||
* The IncrementalWebpackCompiler tracks which entry points have been
|
||||
* requested, and only compiles entry points visited within the last `ttl`
|
||||
* days.
|
||||
*/
|
||||
class IncrementalWebpackCompiler {
|
||||
constructor(historyFilePath, ttl) {
|
||||
this.enabled = true;
|
||||
this.history = new HistoryWithTTL(historyFilePath, ttl);
|
||||
}
|
||||
|
||||
filterEntryPoints(entrypoints) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(entrypoints).map(([entryPoint, paths]) => {
|
||||
if (this.history.isRecentlyVisited(entryPoint)) {
|
||||
return [entryPoint, paths];
|
||||
}
|
||||
return [entryPoint, ['./webpack_non_compiled_placeholder.js']];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
logStatus(totalCount) {
|
||||
log(`Currently compiling route entrypoints: ${this.history.size} of ${totalCount}`);
|
||||
}
|
||||
|
||||
setupMiddleware(app, server) {
|
||||
onRequestEntryPoint(app, (entryPoint) => {
|
||||
const wasVisitedRecently = this.history.onRequestEntryPoint(entryPoint);
|
||||
if (!wasVisitedRecently) {
|
||||
log(`Have not visited ${entryPoint} recently. Adding to compilation.`);
|
||||
|
||||
setTimeout(() => {
|
||||
server.middleware.invalidate(() => {
|
||||
if (server.sockets) {
|
||||
server.sockWrite(server.sockets, 'content-changed');
|
||||
}
|
||||
});
|
||||
}, TIMEOUT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NoopCompiler,
|
||||
HistoryOnlyCompiler,
|
||||
IncrementalWebpackCompiler,
|
||||
};
|
|
@ -0,0 +1,176 @@
|
|||
/* eslint-disable max-classes-per-file, no-underscore-dangle */
|
||||
|
||||
const fs = require('fs');
|
||||
const log = require('./log');
|
||||
|
||||
const ESSENTIAL_ENTRY_POINTS = [
|
||||
// Login page
|
||||
'pages.sessions.new',
|
||||
// Explore page
|
||||
'pages.root',
|
||||
];
|
||||
|
||||
// TODO: Find a way to keep this list up-to-date/relevant.
|
||||
const COMMON_ENTRY_POINTS = [
|
||||
...ESSENTIAL_ENTRY_POINTS,
|
||||
'pages.admin',
|
||||
'pages.admin.dashboard',
|
||||
'pages.dashboard.groups.index',
|
||||
'pages.dashboard.projects.index',
|
||||
'pages.groups.new',
|
||||
'pages.groups.show',
|
||||
'pages.profiles.preferences.show',
|
||||
'pages.projects.commit.show',
|
||||
'pages.projects.edit',
|
||||
'pages.projects.issues.index',
|
||||
'pages.projects.issues.new',
|
||||
'pages.projects.issues.show',
|
||||
'pages.projects.jobs.show',
|
||||
'pages.projects.merge_requests.index',
|
||||
'pages.projects.merge_requests.show',
|
||||
'pages.projects.milestones.index',
|
||||
'pages.projects.new',
|
||||
'pages.projects.pipelines.index',
|
||||
'pages.projects.pipelines.show',
|
||||
'pages.projects.settings.ci_cd.show',
|
||||
'pages.projects.settings.repository.show',
|
||||
'pages.projects.show',
|
||||
'pages.users',
|
||||
];
|
||||
|
||||
/**
|
||||
* The History class is responsible for tracking which entry points have been
|
||||
* requested, and persisting/loading the history to/from disk.
|
||||
*/
|
||||
class History {
|
||||
constructor(historyFilePath) {
|
||||
this._historyFilePath = historyFilePath;
|
||||
this._history = {};
|
||||
|
||||
this._loadHistoryFile();
|
||||
}
|
||||
|
||||
onRequestEntryPoint(entryPoint) {
|
||||
const wasVisitedRecently = this.isRecentlyVisited(entryPoint);
|
||||
|
||||
this._addEntryPoint(entryPoint);
|
||||
|
||||
this._writeHistoryFile();
|
||||
|
||||
return wasVisitedRecently;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isRecentlyVisited() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get size() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
_addEntryPoint(entryPoint) {
|
||||
if (!this._history[entryPoint]) {
|
||||
this._history[entryPoint] = { lastVisit: null, count: 0 };
|
||||
}
|
||||
|
||||
this._history[entryPoint].lastVisit = Date.now();
|
||||
this._history[entryPoint].count += 1;
|
||||
}
|
||||
|
||||
_writeHistoryFile() {
|
||||
try {
|
||||
fs.writeFileSync(this._historyFilePath, JSON.stringify(this._history), 'utf8');
|
||||
} catch (error) {
|
||||
log('Warning – Could not write to history', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_loadHistoryFile() {
|
||||
try {
|
||||
fs.accessSync(this._historyFilePath);
|
||||
} catch (e) {
|
||||
// History file doesn't exist; attempt to seed it, and return early
|
||||
this._seedHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
// History file already exists; attempt to load its contents into memory
|
||||
try {
|
||||
this._history = JSON.parse(fs.readFileSync(this._historyFilePath, 'utf8'));
|
||||
const historySize = Object.keys(this._history).length;
|
||||
log(`Successfully loaded history containing ${historySize} entry points`);
|
||||
} catch (error) {
|
||||
log('Could not load history', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds a reasonable set of approximately the most common entry points to
|
||||
* seed the history. This helps to avoid fresh GDK installs showing the
|
||||
* compiling overlay too often.
|
||||
*/
|
||||
_seedHistory() {
|
||||
log('Seeding history...');
|
||||
COMMON_ENTRY_POINTS.forEach((entryPoint) => this._addEntryPoint(entryPoint));
|
||||
this._writeHistoryFile();
|
||||
}
|
||||
}
|
||||
|
||||
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* The HistoryWithTTL class adds LRU-like behaviour onto the base History
|
||||
* behaviour. Entry points visited within the last `ttl` days are considered
|
||||
* "recent", and therefore should be eagerly compiled.
|
||||
*/
|
||||
class HistoryWithTTL extends History {
|
||||
constructor(historyFilePath, ttl) {
|
||||
super(historyFilePath);
|
||||
this._ttl = ttl;
|
||||
this._calculateRecentEntryPoints();
|
||||
}
|
||||
|
||||
onRequestEntryPoint(entryPoint) {
|
||||
const wasVisitedRecently = super.onRequestEntryPoint(entryPoint);
|
||||
|
||||
this._calculateRecentEntryPoints();
|
||||
|
||||
return wasVisitedRecently;
|
||||
}
|
||||
|
||||
isRecentlyVisited(entryPoint) {
|
||||
return this._recentEntryPoints.has(entryPoint);
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._recentEntryPoints.size;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
_calculateRecentEntryPoints() {
|
||||
const oldestVisitAllowed = Date.now() - MS_PER_DAY * this._ttl;
|
||||
|
||||
const recentEntryPoints = Object.entries(this._history).reduce(
|
||||
(acc, [entryPoint, { lastVisit }]) => {
|
||||
if (lastVisit > oldestVisitAllowed) {
|
||||
acc.push(entryPoint);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
this._recentEntryPoints = new Set([...ESSENTIAL_ENTRY_POINTS, ...recentEntryPoints]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
History,
|
||||
HistoryWithTTL,
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
const { NoopCompiler, HistoryOnlyCompiler, IncrementalWebpackCompiler } = require('./compiler');
|
||||
const log = require('./log');
|
||||
|
||||
module.exports = (recordHistory, enabled, historyFilePath, ttl) => {
|
||||
if (!recordHistory) {
|
||||
log(`Status – disabled`);
|
||||
return new NoopCompiler();
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
log(`Status – enabled, ttl=${ttl}`);
|
||||
return new IncrementalWebpackCompiler(historyFilePath, ttl);
|
||||
}
|
||||
|
||||
log(`Status – history-only`);
|
||||
return new HistoryOnlyCompiler(historyFilePath);
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest);
|
||||
|
||||
module.exports = log;
|
|
@ -64,7 +64,7 @@ constraints(::Constraints::UserUrlConstrainer.new) do
|
|||
get ':username.keys' => 'users#ssh_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
|
||||
|
||||
# Get all GPG keys of user
|
||||
get ':username.gpg' => 'users#gpg_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
|
||||
get ':username.gpg' => 'users#gpg_keys', as: 'user_gpg_keys', constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }
|
||||
|
||||
scope(path: ':username',
|
||||
as: :user,
|
||||
|
|
|
@ -48,6 +48,8 @@ const INCREMENTAL_COMPILER_ENABLED =
|
|||
IS_DEV_SERVER &&
|
||||
process.env.DEV_SERVER_INCREMENTAL &&
|
||||
process.env.DEV_SERVER_INCREMENTAL !== 'false';
|
||||
const INCREMENTAL_COMPILER_TTL = Number(process.env.DEV_SERVER_INCREMENTAL_TTL) || Infinity;
|
||||
const INCREMENTAL_COMPILER_RECORD_HISTORY = IS_DEV_SERVER && !process.env.CI;
|
||||
const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false';
|
||||
const WEBPACK_MEMORY_TEST =
|
||||
process.env.WEBPACK_MEMORY_TEST && process.env.WEBPACK_MEMORY_TEST !== 'false';
|
||||
|
@ -69,8 +71,10 @@ let watchAutoEntries = [];
|
|||
const defaultEntries = ['./main'];
|
||||
|
||||
const incrementalCompiler = createIncrementalWebpackCompiler(
|
||||
INCREMENTAL_COMPILER_RECORD_HISTORY,
|
||||
INCREMENTAL_COMPILER_ENABLED,
|
||||
path.join(CACHE_PATH, 'incremental-webpack-compiler-history.json'),
|
||||
INCREMENTAL_COMPILER_TTL,
|
||||
);
|
||||
|
||||
function generateEntries() {
|
||||
|
@ -157,6 +161,9 @@ const alias = {
|
|||
// the following resolves files which are different between CE and EE
|
||||
ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
|
||||
|
||||
// the following resolves files which are different between CE and JH
|
||||
jh_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
|
||||
|
||||
// override loader path for icons.svg so we do not duplicate this asset
|
||||
'@gitlab/svgs/dist/icons.svg': path.join(
|
||||
ROOT_PATH,
|
||||
|
@ -180,10 +187,13 @@ if (IS_EE) {
|
|||
if (IS_JH) {
|
||||
Object.assign(alias, {
|
||||
jh: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
|
||||
jh_component: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
|
||||
jh_empty_states: path.join(ROOT_PATH, 'jh/app/views/shared/empty_states'),
|
||||
jh_icons: path.join(ROOT_PATH, 'jh/app/views/shared/icons'),
|
||||
jh_images: path.join(ROOT_PATH, 'jh/app/assets/images'),
|
||||
jh_spec: path.join(ROOT_PATH, 'jh/spec/javascripts'),
|
||||
jh_jest: path.join(ROOT_PATH, 'jh/spec/frontend'),
|
||||
jh_else_ce: path.join(ROOT_PATH, 'jh/app/assets/javascripts'),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -519,6 +529,15 @@ module.exports = {
|
|||
);
|
||||
}),
|
||||
|
||||
!IS_JH &&
|
||||
new webpack.NormalModuleReplacementPlugin(/^jh_component\/(.*)\.vue/, (resource) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resource.request = path.join(
|
||||
ROOT_PATH,
|
||||
'app/assets/javascripts/vue_shared/components/empty_component.js',
|
||||
);
|
||||
}),
|
||||
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
|
@ -634,10 +653,12 @@ module.exports = {
|
|||
}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
// This one is used to define window.gon.ee and other things properly in tests:
|
||||
// These are used to define window.gon.ee, window.gon.jh and other things properly in tests:
|
||||
'process.env.IS_EE': JSON.stringify(IS_EE),
|
||||
// This one is used to check against "EE" properly in application code
|
||||
'process.env.IS_JH': JSON.stringify(IS_JH),
|
||||
// These are used to check against "EE" properly in application code
|
||||
IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
|
||||
IS_JH: IS_JH ? 'window.gon && window.gon.jh' : JSON.stringify(false),
|
||||
// This is used by Sourcegraph because these assets are loaded dnamically
|
||||
'process.env.SOURCEGRAPH_PUBLIC_PATH': JSON.stringify(SOURCEGRAPH_PUBLIC_PATH),
|
||||
}),
|
||||
|
|
|
@ -19,6 +19,11 @@ NOTE:
|
|||
The term GPG is used for all OpenPGP/PGP/GPG related material and
|
||||
implementations.
|
||||
|
||||
To view a user's public GPG key, you can:
|
||||
|
||||
- Go to `https://gitlab.example.com/<username>.gpg`.
|
||||
- Select **View public GPG keys** (**{key}**) in the top right of the user's profile.
|
||||
|
||||
GPG verified tags are not supported yet.
|
||||
|
||||
See the [further reading](#further-reading) section for more details on GPG.
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
const IS_EE = require('./config/helpers/is_ee_env');
|
||||
const isESLint = require('./config/helpers/is_eslint');
|
||||
const IS_JH = require('./config/helpers/is_jh_env');
|
||||
|
||||
module.exports = (path, options = {}) => {
|
||||
const {
|
||||
moduleNameMapper: extModuleNameMapper = {},
|
||||
moduleNameMapperEE: extModuleNameMapperEE = {},
|
||||
moduleNameMapperJH: extModuleNameMapperJH = {},
|
||||
} = options;
|
||||
|
||||
const reporters = ['default'];
|
||||
|
@ -29,6 +31,9 @@ module.exports = (path, options = {}) => {
|
|||
testMatch.push(`<rootDir>/ee/${glob}`);
|
||||
}
|
||||
|
||||
if (IS_JH) {
|
||||
testMatch.push(`<rootDir>/jh/${glob}`);
|
||||
}
|
||||
// workaround for eslint-import-resolver-jest only resolving in test files
|
||||
// see https://github.com/JoinColony/eslint-import-resolver-jest#note
|
||||
if (isESLint(module)) {
|
||||
|
@ -41,8 +46,11 @@ module.exports = (path, options = {}) => {
|
|||
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||
'^ee_component(/.*)$':
|
||||
'<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
|
||||
'^jh_component(/.*)$':
|
||||
'<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
|
||||
'^shared_queries(/.*)$': '<rootDir>/app/graphql/queries$1',
|
||||
'^ee_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||
'^jh_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||
'^helpers(/.*)$': '<rootDir>/spec/frontend/__helpers__$1',
|
||||
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
|
||||
[TEST_FIXTURES_PATTERN]: '<rootDir>/tmp/tests/frontend/fixtures$1',
|
||||
|
@ -70,6 +78,19 @@ module.exports = (path, options = {}) => {
|
|||
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
|
||||
}
|
||||
|
||||
if (IS_JH) {
|
||||
const rootDirJH = '<rootDir>/jh/app/assets/javascripts$1';
|
||||
Object.assign(moduleNameMapper, {
|
||||
'^jh(/.*)$': rootDirJH,
|
||||
'^jh_component(/.*)$': rootDirJH,
|
||||
'^jh_else_ce(/.*)$': rootDirJH,
|
||||
'^jh_jest/(.*)$': '<rootDir>/jh/spec/frontend/$1',
|
||||
...extModuleNameMapperJH,
|
||||
});
|
||||
|
||||
collectCoverageFrom.push(rootDirJH.replace('$1', '/**/*.{js,vue}'));
|
||||
}
|
||||
|
||||
const coverageDirectory = () => {
|
||||
if (process.env.CI_NODE_INDEX && process.env.CI_NODE_TOTAL) {
|
||||
return `<rootDir>/coverage-frontend/jest-${process.env.CI_NODE_INDEX}-${process.env.CI_NODE_TOTAL}`;
|
||||
|
@ -107,6 +128,7 @@ module.exports = (path, options = {}) => {
|
|||
testEnvironment: '<rootDir>/spec/frontend/environment.js',
|
||||
testEnvironmentOptions: {
|
||||
IS_EE,
|
||||
IS_JH,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,9 +8,13 @@ module.exports = {
|
|||
moduleNameMapper: {
|
||||
'^test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
|
||||
'^ee_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
|
||||
'^jh_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
|
||||
},
|
||||
moduleNameMapperEE: {
|
||||
'^ee_else_ce_test_helpers(/.*)$': '<rootDir>/ee/spec/frontend_integration/test_helpers$1',
|
||||
},
|
||||
moduleNameMapperJH: {
|
||||
'^jh_else_ce_test_helpers(/.*)$': '<rootDir>/jh/spec/frontend_integration/test_helpers$1',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -66,7 +66,7 @@ module API
|
|||
get ":channel/charts/:file_name.tgz", requirements: FILE_NAME_REQUIREMENTS do
|
||||
authorize_read_package!(authorized_user_project)
|
||||
|
||||
package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").execute.last!
|
||||
package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent!
|
||||
|
||||
track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace)
|
||||
|
||||
|
|
|
@ -89,8 +89,10 @@ module Gitlab
|
|||
# Sentry, instead of silently terminating this thread.
|
||||
Gitlab::ErrorTracking.track_exception(error)
|
||||
|
||||
Gitlab::AppLogger.error(
|
||||
"Service discovery encountered an error: #{error.message}"
|
||||
Gitlab::Database::LoadBalancing::Logger.error(
|
||||
event: :service_discovery_failure,
|
||||
message: "Service discovery encountered an error: #{error.message}",
|
||||
host_list_length: load_balancer.host_list.length
|
||||
)
|
||||
|
||||
# Slightly randomize the retry delay so that, in the case of a total
|
||||
|
|
|
@ -37,6 +37,7 @@ module Gitlab
|
|||
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
|
||||
gon.time_display_relative = true
|
||||
gon.ee = Gitlab.ee?
|
||||
gon.jh = Gitlab.jh?
|
||||
gon.dot_com = Gitlab.com?
|
||||
|
||||
if current_user
|
||||
|
|
|
@ -36989,6 +36989,11 @@ msgstr ""
|
|||
msgid "View project labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "View public GPG key"
|
||||
msgid_plural "View public GPG keys"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "View replaced file @ "
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -429,4 +429,27 @@ RSpec.describe 'User page' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'GPG keys' do
|
||||
context 'when user has verified GPG keys' do
|
||||
let_it_be(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
|
||||
let_it_be(:gpg_key) { create(:gpg_key, user: user, key: GpgHelpers::User1.public_key) }
|
||||
let_it_be(:gpg_key2) { create(:gpg_key, user: user, key: GpgHelpers::User1.public_key2) }
|
||||
|
||||
it 'shows link to public GPG keys' do
|
||||
subject
|
||||
|
||||
expect(page).to have_link('View public GPG keys', href: user_gpg_keys_path(user))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have verified GPG keys' do
|
||||
it 'does not show link to public GPG keys' do
|
||||
subject
|
||||
|
||||
expect(page).not_to have_link('View public GPG key', href: user_gpg_keys_path(user))
|
||||
expect(page).not_to have_link('View public GPG keys', href: user_gpg_keys_path(user))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,42 +6,51 @@ RSpec.describe ::Packages::Helm::PackageFilesFinder do
|
|||
let_it_be(:project1) { create(:project) }
|
||||
let_it_be(:project2) { create(:project) }
|
||||
let_it_be(:helm_package) { create(:helm_package, project: project1) }
|
||||
let_it_be(:helm_package_file) { helm_package.package_files.first }
|
||||
let_it_be(:helm_package_file1) { helm_package.package_files.first }
|
||||
let_it_be(:helm_package_file2) { create(:helm_package_file, package: helm_package) }
|
||||
let_it_be(:debian_package) { create(:debian_package, project: project1) }
|
||||
|
||||
describe '#execute' do
|
||||
let(:project) { project1 }
|
||||
let(:channel) { 'stable' }
|
||||
let(:params) { {} }
|
||||
let(:project) { project1 }
|
||||
let(:channel) { 'stable' }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(project, channel, params).execute }
|
||||
let(:service) { described_class.new(project, channel, params) }
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute }
|
||||
|
||||
context 'with empty params' do
|
||||
it { is_expected.to match_array([helm_package_file]) }
|
||||
it { is_expected.to eq([helm_package_file2, helm_package_file1]) }
|
||||
end
|
||||
|
||||
context 'with another project' do
|
||||
let(:project) { project2 }
|
||||
|
||||
it { is_expected.to match_array([]) }
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
|
||||
context 'with another channel' do
|
||||
let(:channel) { 'staging' }
|
||||
|
||||
it { is_expected.to match_array([]) }
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
|
||||
context 'with file_name' do
|
||||
let(:params) { { file_name: helm_package_file.file_name } }
|
||||
context 'with matching file_name' do
|
||||
let(:params) { { file_name: helm_package_file1.file_name } }
|
||||
|
||||
it { is_expected.to match_array([helm_package_file]) }
|
||||
it { is_expected.to eq([helm_package_file2, helm_package_file1]) }
|
||||
end
|
||||
|
||||
context 'with another file_name' do
|
||||
let(:params) { { file_name: 'foobar.tgz' } }
|
||||
|
||||
it { is_expected.to match_array([]) }
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#most_recent!' do
|
||||
subject { service.most_recent! }
|
||||
|
||||
it { is_expected.to eq(helm_package_file2) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -349,7 +349,7 @@ describe('Tracking', () => {
|
|||
it('includes experiment data if linked to an experiment', () => {
|
||||
const mockExperimentData = {
|
||||
variant: 'candidate',
|
||||
experiment: 'repo_integrations_link',
|
||||
experiment: 'example',
|
||||
key: '2bff73f6bb8cc11156c50a8ba66b9b8b',
|
||||
};
|
||||
getExperimentData.mockReturnValue(mockExperimentData);
|
||||
|
|
|
@ -7,7 +7,12 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import createFlash from '~/flash';
|
||||
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
|
||||
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
|
||||
import { mockSuggestedColors, createLabelSuccessfulResponse } from './mock_data';
|
||||
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import {
|
||||
mockSuggestedColors,
|
||||
createLabelSuccessfulResponse,
|
||||
labelsQueryResponse,
|
||||
} from './mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
|
@ -44,6 +49,14 @@ describe('DropdownContentsCreateView', () => {
|
|||
|
||||
const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
|
||||
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: projectLabelsQuery,
|
||||
data: labelsQueryResponse.data,
|
||||
variables: {
|
||||
fullPath: '',
|
||||
searchTerm: '',
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMount(DropdownContentsCreateView, {
|
||||
localVue,
|
||||
|
|
|
@ -137,12 +137,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
|
||||
expect(findLabels().at(0).attributes('islabelset')).toBe('true');
|
||||
});
|
||||
|
||||
it('emits `closeDropdown event` when Esc button is pressed', () => {
|
||||
findDropdownWrapper().trigger('keydown.esc');
|
||||
|
||||
expect(wrapper.emitted('closeDropdown')).toEqual([[selectedLabels]]);
|
||||
});
|
||||
});
|
||||
|
||||
it('when search returns 0 results', async () => {
|
||||
|
@ -205,7 +199,7 @@ describe('DropdownContentsLabelsView', () => {
|
|||
});
|
||||
|
||||
it('emits `toggleDropdownContentsCreateView` event on create label button click', () => {
|
||||
findCreateLabelButton().vm.$emit('click');
|
||||
findCreateLabelButton().vm.$emit('click', new MouseEvent('click'));
|
||||
|
||||
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
|
||||
});
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
|
||||
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
const createComponent = (initialState = mockConfig) => {
|
||||
const store = new Vuex.Store(labelsSelectModule());
|
||||
|
||||
store.dispatch('setInitialState', initialState);
|
||||
|
||||
return shallowMount(DropdownTitle, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: {
|
||||
labelsSelectInProgress: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('DropdownTitle', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component container element with string "Labels"', () => {
|
||||
expect(wrapper.text()).toContain('Labels');
|
||||
});
|
||||
|
||||
it('renders edit link', () => {
|
||||
const editBtnEl = wrapper.find(GlButton);
|
||||
|
||||
expect(editBtnEl.exists()).toBe(true);
|
||||
expect(editBtnEl.text()).toBe('Edit');
|
||||
});
|
||||
|
||||
it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
|
||||
wrapper.setProps({
|
||||
labelsSelectInProgress: true,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,7 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { isInViewport } from '~/lib/utils/common_utils';
|
||||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
|
||||
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
|
||||
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
|
||||
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
|
||||
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
|
||||
|
@ -32,11 +28,13 @@ describe('LabelsSelectRoot', () => {
|
|||
store,
|
||||
propsData: config,
|
||||
stubs: {
|
||||
'dropdown-contents': DropdownContents,
|
||||
DropdownContents,
|
||||
SidebarEditableItem,
|
||||
},
|
||||
provide: {
|
||||
iid: '1',
|
||||
projectPath: 'test',
|
||||
canUpdate: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -49,145 +47,44 @@ describe('LabelsSelectRoot', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('handleDropdownClose', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
|
||||
wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
|
||||
|
||||
expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
|
||||
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
|
||||
wrapper.vm.handleDropdownClose([]);
|
||||
|
||||
expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
|
||||
expect(wrapper.emitted().onDropdownClose).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCollapsedValueClick', () => {
|
||||
it('emits `toggleCollapse` event on component', () => {
|
||||
createComponent();
|
||||
wrapper.vm.handleCollapsedValueClick();
|
||||
|
||||
expect(wrapper.emitted().toggleCollapse).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component with classes `labels-select-wrapper position-relative`', () => {
|
||||
createComponent();
|
||||
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
|
||||
});
|
||||
|
||||
it.each`
|
||||
variant | cssClass
|
||||
${'standalone'} | ${'is-standalone'}
|
||||
${'embedded'} | ${'is-embedded'}
|
||||
`(
|
||||
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
|
||||
({ variant, cssClass }) => {
|
||||
createComponent({
|
||||
...mockConfig,
|
||||
variant,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.classes()).toContain(cssClass);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
|
||||
createComponent();
|
||||
await wrapper.vm.$nextTick;
|
||||
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-title` component', async () => {
|
||||
createComponent();
|
||||
await wrapper.vm.$nextTick;
|
||||
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-value` component', async () => {
|
||||
createComponent(mockConfig, {
|
||||
default: 'None',
|
||||
});
|
||||
await wrapper.vm.$nextTick;
|
||||
|
||||
const valueComp = wrapper.find(DropdownValue);
|
||||
|
||||
expect(valueComp.exists()).toBe(true);
|
||||
expect(valueComp.text()).toBe('None');
|
||||
});
|
||||
|
||||
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
|
||||
createComponent();
|
||||
wrapper.vm.$store.dispatch('toggleDropdownButton');
|
||||
await wrapper.vm.$nextTick;
|
||||
expect(wrapper.find(DropdownButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
|
||||
createComponent();
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContents');
|
||||
await wrapper.vm.$nextTick;
|
||||
expect(wrapper.find(DropdownContents).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('sets content direction based on viewport', () => {
|
||||
describe.each(Object.values(DropdownVariant))(
|
||||
'when labels variant is "%s"',
|
||||
({ variant }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ ...mockConfig, variant });
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContents');
|
||||
});
|
||||
|
||||
it('set direction when out of viewport', () => {
|
||||
isInViewport.mockImplementation(() => false);
|
||||
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set direction when inside of viewport', () => {
|
||||
isInViewport.mockImplementation(() => true);
|
||||
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
|
||||
it('renders component with classes `labels-select-wrapper position-relative`', () => {
|
||||
createComponent();
|
||||
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
await wrapper.setProps({ isEditing: true });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
|
||||
expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'position-relative']);
|
||||
});
|
||||
|
||||
it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
|
||||
it.each`
|
||||
variant | cssClass
|
||||
${'standalone'} | ${'is-standalone'}
|
||||
${'embedded'} | ${'is-embedded'}
|
||||
`(
|
||||
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
|
||||
({ variant, cssClass }) => {
|
||||
createComponent({
|
||||
...mockConfig,
|
||||
variant,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.classes()).toContain(cssClass);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
|
||||
createComponent();
|
||||
await wrapper.vm.$nextTick;
|
||||
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
|
||||
});
|
||||
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
await wrapper.setProps({ isEditing: false });
|
||||
it('renders `dropdown-value` component', async () => {
|
||||
createComponent(mockConfig, {
|
||||
default: 'None',
|
||||
});
|
||||
await wrapper.vm.$nextTick;
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
const valueComp = wrapper.find(DropdownValue);
|
||||
|
||||
expect(valueComp.exists()).toBe(true);
|
||||
expect(valueComp.text()).toBe('None');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,9 +83,7 @@ export const createLabelSuccessfulResponse = {
|
|||
id: 'gid://gitlab/ProjectLabel/126',
|
||||
color: '#dc143c',
|
||||
description: null,
|
||||
descriptionHtml: '',
|
||||
title: 'ewrwrwer',
|
||||
textColor: '#FFFFFF',
|
||||
__typename: 'Label',
|
||||
},
|
||||
errors: [],
|
||||
|
|
|
@ -139,6 +139,10 @@ RSpec.describe Packages::PackageFile, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.most_recent!' do
|
||||
it { expect(described_class.most_recent!).to eq(debian_package.package_files.last) }
|
||||
end
|
||||
|
||||
describe '#update_file_store callback' do
|
||||
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
|
||||
|
||||
|
|
|
@ -3389,17 +3389,32 @@ RSpec.describe User do
|
|||
end
|
||||
|
||||
describe '#membership_groups' do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:parent_group) { create(:group) }
|
||||
let!(:child_group) { create(:group, parent: parent_group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
parent_group.add_user(user, Gitlab::Access::MAINTAINER)
|
||||
let_it_be(:parent_group) do
|
||||
create(:group).tap do |g|
|
||||
g.add_user(user, Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:child_group) { create(:group, parent: parent_group) }
|
||||
let_it_be(:other_group) { create(:group) }
|
||||
|
||||
subject { user.membership_groups }
|
||||
|
||||
it { is_expected.to contain_exactly parent_group, child_group }
|
||||
shared_examples 'returns groups where the user is a member' do
|
||||
specify { is_expected.to contain_exactly(parent_group, child_group) }
|
||||
end
|
||||
|
||||
it_behaves_like 'returns groups where the user is a member'
|
||||
|
||||
context 'when feature flag :linear_user_membership_groups is disabled' do
|
||||
before do
|
||||
stub_feature_flags(linear_user_membership_groups: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returns groups where the user is a member'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#authorizations_for_projects' do
|
||||
|
|
|
@ -649,36 +649,18 @@ RSpec.describe ProjectPresenter do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'experiment(:repo_integrations_link)' do
|
||||
context 'when enabled' do
|
||||
before do
|
||||
stub_experiments(repo_integrations_link: :candidate)
|
||||
end
|
||||
it 'includes a button to configure integrations for maintainers' do
|
||||
project.add_maintainer(user)
|
||||
|
||||
it 'includes a button to configure integrations for maintainers' do
|
||||
project.add_maintainer(user)
|
||||
expect(empty_repo_statistics_buttons.map(&:label)).to include(
|
||||
a_string_including('Configure Integration')
|
||||
)
|
||||
end
|
||||
|
||||
expect(empty_repo_statistics_buttons.map(&:label)).to include(
|
||||
a_string_including('Configure Integration')
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not include a button if not a maintainer' do
|
||||
expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
|
||||
a_string_including('Configure Integration')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
it 'does not include a button' do
|
||||
project.add_maintainer(user)
|
||||
|
||||
expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
|
||||
a_string_including('Configure Integration')
|
||||
)
|
||||
end
|
||||
end
|
||||
it 'does not include a button if not a maintainer' do
|
||||
expect(empty_repo_statistics_buttons.map(&:label)).not_to include(
|
||||
a_string_including('Configure Integration')
|
||||
)
|
||||
end
|
||||
|
||||
context 'for a developer' do
|
||||
|
|
|
@ -9,16 +9,18 @@ RSpec.describe API::HelmPackages do
|
|||
let_it_be_with_reload(:project) { create(:project, :public) }
|
||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
||||
let_it_be(:package) { create(:helm_package, project: project) }
|
||||
let_it_be(:package) { create(:helm_package, project: project, without_package_files: true) }
|
||||
let_it_be(:package_file1) { create(:helm_package_file, package: package) }
|
||||
let_it_be(:package_file2) { create(:helm_package_file, package: package) }
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do
|
||||
it_behaves_like 'handling helm chart index requests' do
|
||||
let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/index.yaml" }
|
||||
let(:url) { "/projects/#{project.id}/packages/helm/stable/index.yaml" }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/helm/:channel/charts/:file_name.tgz' do
|
||||
let(:url) { "/projects/#{project.id}/packages/helm/#{package.package_files.first.helm_channel}/charts/#{package.name}-#{package.version}.tgz" }
|
||||
let(:url) { "/projects/#{project.id}/packages/helm/stable/charts/#{package.name}-#{package.version}.tgz" }
|
||||
|
||||
subject { get api(url), headers: headers }
|
||||
|
||||
|
|
|
@ -72,13 +72,10 @@ ALLOW_LIST = Set.new(YAML.load_file(Rails.root.join('.cross-join-allowlist.yml')
|
|||
RSpec.configure do |config|
|
||||
config.include(::Database::PreventCrossJoins::SpecHelpers)
|
||||
|
||||
config.around do |example|
|
||||
# TODO: remove `:prevent_cross_joins` to enable the check by default
|
||||
config.around(:each, :prevent_cross_joins) do |example|
|
||||
Thread.current[:has_cross_join_exception] = false
|
||||
|
||||
if ALLOW_LIST.include?(example.file_path)
|
||||
example.run
|
||||
else
|
||||
with_cross_joins_prevented { example.run }
|
||||
end
|
||||
with_cross_joins_prevented { example.run }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status
|
|||
|
||||
package_entry = yaml_response['entries'][package.name]
|
||||
|
||||
expect(package_entry.length).to eq(1)
|
||||
expect(package_entry.length).to eq(2)
|
||||
expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls')
|
||||
expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db')
|
||||
expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"])
|
||||
|
@ -174,6 +174,13 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta
|
|||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member
|
||||
|
||||
expect_next_found_instance_of(::Packages::PackageFile) do |package_file|
|
||||
expect(package_file).to receive(:file).and_wrap_original do |m, *args|
|
||||
expect(package_file.id).to eq(package_file2.id)
|
||||
m.call(*args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Database::PreventCrossJoins do
|
||||
context 'when running in a default scope' do
|
||||
context 'when running in :prevent_cross_joins scope', :prevent_cross_joins do
|
||||
context 'when only non-CI tables are used' do
|
||||
it 'does not raise exception' do
|
||||
expect { main_only_query }.not_to raise_error
|
||||
|
@ -32,6 +32,14 @@ RSpec.describe Database::PreventCrossJoins do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when running in a default scope' do
|
||||
context 'when CI and non-CI tables are used' do
|
||||
it 'does not raise exception' do
|
||||
expect { main_and_ci_query }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def main_only_query
|
||||
|
|
|
@ -23,10 +23,10 @@ RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
|
|||
|
||||
subject { described_class.new.perform(channel, package_file_id) }
|
||||
|
||||
shared_examples 'handling error' do
|
||||
shared_examples 'handling error' do |error_class = Packages::Helm::ExtractFileMetadataService::ExtractionError|
|
||||
it 'mark the package as errored', :aggregate_failures do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
||||
instance_of(Packages::Helm::ExtractFileMetadataService::ExtractionError),
|
||||
instance_of(error_class),
|
||||
project_id: package_file.package.project_id
|
||||
)
|
||||
expect { subject }
|
||||
|
@ -88,5 +88,15 @@ RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
|
|||
|
||||
it_behaves_like 'handling error'
|
||||
end
|
||||
|
||||
context 'with an invalid Chart.yaml' do
|
||||
before do
|
||||
expect_next_instance_of(Gem::Package::TarReader::Entry) do |entry|
|
||||
expect(entry).to receive(:read).and_return('{}')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'handling error', ActiveRecord::RecordInvalid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue