Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fe30598cbd
commit
e612fbe905
|
@ -112,3 +112,7 @@ overrides:
|
|||
import/no-nodejs-modules: off
|
||||
filenames/match-regex: off
|
||||
no-console: off
|
||||
- files:
|
||||
- '*.stories.js'
|
||||
rules:
|
||||
filenames/match-regex: off
|
||||
|
|
|
@ -344,3 +344,18 @@ startup-css-check as-if-foss:
|
|||
needs:
|
||||
- job: "compile-test-assets as-if-foss"
|
||||
- job: "rspec frontend_fixture as-if-foss"
|
||||
|
||||
compile-storybook:
|
||||
extends:
|
||||
- .compile-assets-base
|
||||
script:
|
||||
- source scripts/utils.sh
|
||||
- cd storybook/
|
||||
- run_timed_command "retry yarn install --frozen-lockfile"
|
||||
- yarn build
|
||||
artifacts:
|
||||
name: storybook
|
||||
expire_in: 31d
|
||||
when: always
|
||||
paths:
|
||||
- storybook/public
|
||||
|
|
|
@ -8,12 +8,14 @@ pages:
|
|||
- coverage-frontend
|
||||
- karma
|
||||
- compile-production-assets
|
||||
- compile-storybook
|
||||
script:
|
||||
- mv public/ .public/
|
||||
- mkdir public/
|
||||
- mv coverage/ public/coverage-ruby/ || true
|
||||
- mv coverage-frontend/ public/coverage-frontend/ || true
|
||||
- mv coverage-javascript/ public/coverage-javascript/ || true
|
||||
- mv storybook/public public/storybook || true
|
||||
- cp .public/assets/application-*.css public/application.css || true
|
||||
- cp .public/assets/application-*.css.gz public/application.css.gz || true
|
||||
artifacts:
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { GlDrawer } from '@gitlab/ui';
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
|
||||
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
||||
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
|
||||
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||
import { ISSUABLE } from '~/boards/constants';
|
||||
|
@ -23,11 +23,9 @@ export default {
|
|||
BoardSidebarLabelsSelect,
|
||||
BoardSidebarDueDate,
|
||||
SidebarSubscriptionsWidget,
|
||||
BoardSidebarMilestoneSelect,
|
||||
SidebarDropdownWidget,
|
||||
BoardSidebarWeightInput: () =>
|
||||
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
|
||||
SidebarDropdownWidget: () =>
|
||||
import('ee_component/sidebar/components/sidebar_dropdown_widget.vue'),
|
||||
},
|
||||
inject: {
|
||||
multipleAssigneesFeatureAvailable: {
|
||||
|
@ -97,7 +95,14 @@ export default {
|
|||
data-testid="sidebar-epic"
|
||||
/>
|
||||
<div>
|
||||
<board-sidebar-milestone-select />
|
||||
<sidebar-dropdown-widget
|
||||
:iid="activeBoardItem.iid"
|
||||
issuable-attribute="milestone"
|
||||
:workspace-path="projectPathForActiveIssue"
|
||||
:attr-workspace-path="projectPathForActiveIssue"
|
||||
:issuable-type="issuableType"
|
||||
data-testid="sidebar-milestones"
|
||||
/>
|
||||
<sidebar-dropdown-widget
|
||||
v-if="iterationFeatureAvailable"
|
||||
:iid="activeBoardItem.iid"
|
||||
|
|
|
@ -135,6 +135,13 @@ export default {
|
|||
resolveWithIssuePath() {
|
||||
return !this.discussionResolved ? this.discussion.resolve_with_issue_path : '';
|
||||
},
|
||||
canShowReplyActions() {
|
||||
if (this.shouldRenderDiffs && !this.discussion.diff_file.diff_refs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('startReplying', this.onStartReplying);
|
||||
|
@ -263,7 +270,7 @@ export default {
|
|||
:draft="draftForDiscussion(discussion.reply_id)"
|
||||
/>
|
||||
<div
|
||||
v-else-if="showReplies"
|
||||
v-else-if="canShowReplyActions && showReplies"
|
||||
:class="{ 'is-replying': isReplying }"
|
||||
class="discussion-reply-holder gl-border-t-0! clearfix"
|
||||
>
|
||||
|
|
|
@ -26,10 +26,10 @@ const PRIVATE_VISIBILITY = 'private';
|
|||
const INTERNAL_VISIBILITY = 'internal';
|
||||
const PUBLIC_VISIBILITY = 'public';
|
||||
|
||||
const ALLOWED_VISIBILITY = {
|
||||
private: [PRIVATE_VISIBILITY],
|
||||
internal: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY],
|
||||
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
|
||||
const VISIBILITY_LEVEL = {
|
||||
[PRIVATE_VISIBILITY]: 0,
|
||||
[INTERNAL_VISIBILITY]: 10,
|
||||
[PUBLIC_VISIBILITY]: 20,
|
||||
};
|
||||
|
||||
const initFormField = ({ value, required = true, skipValidation = false }) => ({
|
||||
|
@ -124,14 +124,23 @@ export default {
|
|||
projectUrl() {
|
||||
return `${gon.gitlab_url}/`;
|
||||
},
|
||||
projectAllowedVisibility() {
|
||||
return ALLOWED_VISIBILITY[this.projectVisibility];
|
||||
projectVisibilityLevel() {
|
||||
return VISIBILITY_LEVEL[this.projectVisibility];
|
||||
},
|
||||
namespaceAllowedVisibility() {
|
||||
return (
|
||||
ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
|
||||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
|
||||
);
|
||||
namespaceVisibilityLevel() {
|
||||
const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY;
|
||||
return VISIBILITY_LEVEL[visibility];
|
||||
},
|
||||
visibilityLevelCap() {
|
||||
return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
|
||||
},
|
||||
allowedVisibilityLevels() {
|
||||
return Object.entries(VISIBILITY_LEVEL).reduce((levels, [levelName, levelValue]) => {
|
||||
if (levelValue <= this.visibilityLevelCap) {
|
||||
levels.push(levelName);
|
||||
}
|
||||
return levels;
|
||||
}, []);
|
||||
},
|
||||
visibilityLevels() {
|
||||
return [
|
||||
|
@ -179,11 +188,8 @@ export default {
|
|||
const { data } = await axios.get(this.endpoint);
|
||||
this.namespaces = data.namespaces;
|
||||
},
|
||||
isVisibilityLevelDisabled(visibilityLevel) {
|
||||
return !(
|
||||
this.projectAllowedVisibility.includes(visibilityLevel) &&
|
||||
this.namespaceAllowedVisibility.includes(visibilityLevel)
|
||||
);
|
||||
isVisibilityLevelDisabled(visibility) {
|
||||
return !this.allowedVisibilityLevels.includes(visibility);
|
||||
},
|
||||
async onSubmit() {
|
||||
this.form.showValidation = true;
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
<script>
|
||||
import {
|
||||
GlLink,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownText,
|
||||
GlSearchBoxByType,
|
||||
GlDropdownDivider,
|
||||
GlLoadingIcon,
|
||||
GlIcon,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import {
|
||||
IssuableAttributeState,
|
||||
IssuableAttributeType,
|
||||
issuableAttributesQueries,
|
||||
noAttributeId,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
noAttributeId,
|
||||
IssuableAttributeState,
|
||||
issuableAttributesQueries,
|
||||
i18n: {
|
||||
[IssuableAttributeType.Milestone]: __('Milestone'),
|
||||
none: __('None'),
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
SidebarEditableItem,
|
||||
GlLink,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownText,
|
||||
GlDropdownDivider,
|
||||
GlSearchBoxByType,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
inject: {
|
||||
isClassicSidebar: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
issuableAttribute: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return [IssuableAttributeType.Milestone].includes(value);
|
||||
},
|
||||
},
|
||||
workspacePath: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
iid: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
attrWorkspacePath: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value === IssuableType.Issue;
|
||||
},
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
currentAttribute: {
|
||||
query() {
|
||||
const { current } = this.issuableAttributeQuery;
|
||||
const { query } = current[this.issuableType];
|
||||
|
||||
return query;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.workspacePath,
|
||||
iid: this.iid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data?.workspace?.issuable.attribute;
|
||||
},
|
||||
error(error) {
|
||||
createFlash({
|
||||
message: this.i18n.currentFetchError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
attributesList: {
|
||||
query() {
|
||||
const { list } = this.issuableAttributeQuery;
|
||||
const { query } = list[this.issuableType];
|
||||
|
||||
return query;
|
||||
},
|
||||
skip() {
|
||||
return !this.editing;
|
||||
},
|
||||
debounce: 250,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.attrWorkspacePath,
|
||||
title: this.searchTerm,
|
||||
state: this.$options.IssuableAttributeState[this.issuableAttribute],
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
if (data?.workspace) {
|
||||
return data?.workspace?.attributes.nodes;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
error(error) {
|
||||
createFlash({ message: this.i18n.listFetchError, captureError: true, error });
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
editing: false,
|
||||
updating: false,
|
||||
selectedTitle: null,
|
||||
currentAttribute: null,
|
||||
attributesList: [],
|
||||
tracking: {
|
||||
label: 'right_sidebar',
|
||||
event: 'click_edit_button',
|
||||
property: this.issuableAttribute,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
issuableAttributeQuery() {
|
||||
return this.$options.issuableAttributesQueries[this.issuableAttribute];
|
||||
},
|
||||
attributeTitle() {
|
||||
return this.currentAttribute?.title || this.i18n.noAttribute;
|
||||
},
|
||||
attributeUrl() {
|
||||
return this.currentAttribute?.webUrl;
|
||||
},
|
||||
dropdownText() {
|
||||
return this.currentAttribute
|
||||
? this.currentAttribute?.title
|
||||
: this.$options.i18n[this.issuableAttribute];
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.currentAttribute.loading;
|
||||
},
|
||||
emptyPropsList() {
|
||||
return this.attributesList.length === 0;
|
||||
},
|
||||
attributeTypeTitle() {
|
||||
return this.$options.i18n[this.issuableAttribute];
|
||||
},
|
||||
i18n() {
|
||||
return {
|
||||
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
|
||||
issuableAttribute: this.issuableAttribute,
|
||||
}),
|
||||
assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), {
|
||||
issuableAttribute: this.issuableAttribute,
|
||||
}),
|
||||
noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), {
|
||||
issuableAttribute: this.issuableAttribute,
|
||||
}),
|
||||
updateError: sprintf(
|
||||
s__(
|
||||
'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.',
|
||||
),
|
||||
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
|
||||
),
|
||||
listFetchError: sprintf(
|
||||
s__(
|
||||
'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.',
|
||||
),
|
||||
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
|
||||
),
|
||||
currentFetchError: sprintf(
|
||||
s__(
|
||||
'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.',
|
||||
),
|
||||
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateAttribute(attributeId) {
|
||||
if (this.currentAttribute === null && attributeId === null) return;
|
||||
if (attributeId === this.currentAttribute?.id) return;
|
||||
|
||||
this.updating = true;
|
||||
|
||||
const selectedAttribute =
|
||||
Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId);
|
||||
this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.$options.i18n.none;
|
||||
|
||||
const { current } = this.issuableAttributeQuery;
|
||||
const { mutation } = current[this.issuableType];
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation,
|
||||
variables: {
|
||||
fullPath: this.workspacePath,
|
||||
attributeId:
|
||||
this.issuableAttribute === IssuableAttributeType.Milestone
|
||||
? getIdFromGraphQLId(attributeId)
|
||||
: attributeId,
|
||||
iid: this.iid,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.issuableSetAttribute?.errors?.length) {
|
||||
createFlash({
|
||||
message: data.issuableSetAttribute.errors[0],
|
||||
captureError: true,
|
||||
error: data.issuableSetAttribute.errors[0],
|
||||
});
|
||||
} else {
|
||||
this.$emit('attribute-updated', data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
createFlash({ message: this.i18n.updateError, captureError: true, error });
|
||||
})
|
||||
.finally(() => {
|
||||
this.updating = false;
|
||||
this.searchTerm = '';
|
||||
this.selectedTitle = null;
|
||||
});
|
||||
},
|
||||
isAttributeChecked(attributeId = undefined) {
|
||||
return (
|
||||
attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
|
||||
);
|
||||
},
|
||||
showDropdown() {
|
||||
this.$refs.newDropdown.show();
|
||||
},
|
||||
handleOpen() {
|
||||
this.editing = true;
|
||||
this.showDropdown();
|
||||
},
|
||||
handleClose() {
|
||||
this.editing = false;
|
||||
},
|
||||
setFocus() {
|
||||
this.$refs.search.focusInput();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<sidebar-editable-item
|
||||
ref="editable"
|
||||
:title="attributeTypeTitle"
|
||||
:data-testid="`${issuableAttribute}-edit`"
|
||||
:tracking="tracking"
|
||||
:loading="updating || loading"
|
||||
@open="handleOpen"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #collapsed>
|
||||
<div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon">
|
||||
<gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" />
|
||||
<span class="collapse-truncated-title">{{ attributeTitle }}</span>
|
||||
</div>
|
||||
<div
|
||||
:data-testid="`select-${issuableAttribute}`"
|
||||
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
|
||||
>
|
||||
<span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span>
|
||||
<span v-else-if="!currentAttribute" class="gl-text-gray-500">
|
||||
{{ $options.i18n.none }}
|
||||
</span>
|
||||
<gl-link v-else class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl">
|
||||
{{ attributeTitle }}
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<gl-dropdown
|
||||
ref="newDropdown"
|
||||
lazy
|
||||
:header-text="i18n.assignAttribute"
|
||||
:text="dropdownText"
|
||||
:loading="loading"
|
||||
class="gl-w-full"
|
||||
@shown="setFocus"
|
||||
>
|
||||
<gl-search-box-by-type ref="search" v-model="searchTerm" />
|
||||
<gl-dropdown-item
|
||||
:data-testid="`no-${issuableAttribute}-item`"
|
||||
:is-check-item="true"
|
||||
:is-checked="isAttributeChecked($options.noAttributeId)"
|
||||
@click="updateAttribute($options.noAttributeId)"
|
||||
>
|
||||
{{ i18n.noAttribute }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-loading-icon
|
||||
v-if="$apollo.queries.attributesList.loading"
|
||||
class="gl-py-4"
|
||||
data-testid="loading-icon-dropdown"
|
||||
/>
|
||||
<template v-else>
|
||||
<gl-dropdown-text v-if="emptyPropsList">
|
||||
{{ i18n.noAttributesFound }}
|
||||
</gl-dropdown-text>
|
||||
<gl-dropdown-item
|
||||
v-for="attrItem in attributesList"
|
||||
:key="attrItem.id"
|
||||
:is-check-item="true"
|
||||
:is-checked="isAttributeChecked(attrItem.id)"
|
||||
:data-testid="`${issuableAttribute}-items`"
|
||||
@click="updateAttribute(attrItem.id)"
|
||||
>
|
||||
{{ attrItem.title }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
</sidebar-editable-item>
|
||||
</template>
|
|
@ -29,6 +29,9 @@ import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries
|
|||
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
|
||||
import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
|
||||
import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
|
||||
import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
|
||||
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
|
||||
import projectMilestonesQuery from './queries/project_milestones.query.graphql';
|
||||
|
||||
export const ASSIGNEES_DEBOUNCE_DELAY = 250;
|
||||
|
||||
|
@ -143,3 +146,33 @@ export const timelogQueries = {
|
|||
query: getMrTimelogsQuery,
|
||||
},
|
||||
};
|
||||
|
||||
export const noAttributeId = null;
|
||||
|
||||
export const issuableMilestoneQueries = {
|
||||
[IssuableType.Issue]: {
|
||||
query: projectIssueMilestoneQuery,
|
||||
mutation: projectIssueMilestoneMutation,
|
||||
},
|
||||
};
|
||||
|
||||
export const milestonesQueries = {
|
||||
[IssuableType.Issue]: {
|
||||
query: projectMilestonesQuery,
|
||||
},
|
||||
};
|
||||
|
||||
export const IssuableAttributeType = {
|
||||
Milestone: 'milestone',
|
||||
};
|
||||
|
||||
export const IssuableAttributeState = {
|
||||
[IssuableAttributeType.Milestone]: 'active',
|
||||
};
|
||||
|
||||
export const issuableAttributesQueries = {
|
||||
[IssuableAttributeType.Milestone]: {
|
||||
current: issuableMilestoneQueries,
|
||||
list: milestonesQueries,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
fragment MilestoneFragment on Milestone {
|
||||
id
|
||||
title
|
||||
webUrl: webPath
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attributeId: ID) {
|
||||
issuableSetAttribute: updateIssue(
|
||||
input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId }
|
||||
) {
|
||||
__typename
|
||||
errors
|
||||
issuable: issue {
|
||||
__typename
|
||||
id
|
||||
attribute: milestone {
|
||||
title
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
#import "./milestone.fragment.graphql"
|
||||
|
||||
query projectIssueMilestone($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: issue(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
attribute: milestone {
|
||||
...MilestoneFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
#import "./milestone.fragment.graphql"
|
||||
|
||||
query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
attributes: milestones(searchTitle: $title, state: $state) {
|
||||
nodes {
|
||||
...MilestoneFragment
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
|
||||
import TodoButton from './todo_button.vue';
|
||||
|
||||
export default {
|
||||
component: TodoButton,
|
||||
title: 'vue_shared/components/todo_button',
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
components: { TodoButton },
|
||||
props: Object.keys(argTypes),
|
||||
template: '<todo-button v-bind="$props" v-on="$props" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.argTypes = {
|
||||
isTodo: {
|
||||
description: 'True if to-do is unresolved (i.e. not "done")',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
click: { action: 'clicked' },
|
||||
};
|
|
@ -63,6 +63,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def edit
|
||||
exclude_legacy_flags_check
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -158,4 +159,12 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
|
|||
render json: { message: messages },
|
||||
status: status
|
||||
end
|
||||
|
||||
def exclude_legacy_flags_check
|
||||
if Feature.enabled?(:remove_legacy_flags, project, default_enabled: :yaml) &&
|
||||
Feature.disabled?(:remove_legacy_flags_override, project, default_enabled: :yaml) &&
|
||||
feature_flag.legacy_flag?
|
||||
not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1076,6 +1076,10 @@ module Ci
|
|||
::Ci::PendingBuild.where(build_id: self.id)
|
||||
end
|
||||
|
||||
def create_queuing_entry!
|
||||
::Ci::PendingBuild.upsert_from_build!(self)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def run_status_commit_hooks!
|
||||
|
|
|
@ -9,7 +9,7 @@ class MergeRequestDiffEntity < Grape::Entity
|
|||
@merge_request_diffs = options[:merge_request_diffs]
|
||||
diff = options[:merge_request_diff]
|
||||
|
||||
next unless diff.present?
|
||||
next unless @merge_request_diffs.include?(diff)
|
||||
next unless @merge_request_diffs.size > 1
|
||||
|
||||
version_index(merge_request_diff)
|
||||
|
|
|
@ -287,15 +287,11 @@ module Ci
|
|||
.order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def builds_for_project_runner
|
||||
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def builds_for_group_runner
|
||||
# Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL`
|
||||
groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces)
|
||||
|
@ -307,17 +303,23 @@ module Ci
|
|||
.without_deleted
|
||||
new_builds.where(project: projects).order('id ASC')
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def running_builds_for_shared_runners
|
||||
Ci::Build.running.where(runner: Ci::Runner.instance_type)
|
||||
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
|
||||
end
|
||||
|
||||
def all_builds
|
||||
if Feature.enabled?(:ci_pending_builds_queue_join, runner, default_enabled: :yaml)
|
||||
Ci::Build.joins(:queuing_entry)
|
||||
else
|
||||
Ci::Build.all
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def new_builds
|
||||
builds = Ci::Build.pending.unstarted
|
||||
builds = all_builds.pending.unstarted
|
||||
builds = builds.ref_protected if runner.ref_protected?
|
||||
builds
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ module Ci
|
|||
raise InvalidQueueTransition unless transition.to == 'pending'
|
||||
|
||||
transition.within_transaction do
|
||||
result = ::Ci::PendingBuild.upsert_from_build!(build)
|
||||
result = build.create_queuing_entry!
|
||||
|
||||
unless result.empty?
|
||||
metrics.increment_queue_operation(:build_queue_push)
|
||||
|
|
|
@ -9,6 +9,8 @@ module Deployments
|
|||
delegate :variables, to: :deployable
|
||||
delegate :options, to: :deployable, allow_nil: true
|
||||
|
||||
EnvironmentUpdateFailure = Class.new(StandardError)
|
||||
|
||||
def initialize(deployment)
|
||||
@deployment = deployment
|
||||
@deployable = deployment.deployable
|
||||
|
@ -31,8 +33,18 @@ module Deployments
|
|||
renew_deployment_tier
|
||||
environment.fire_state_event(action)
|
||||
|
||||
if environment.save && !environment.stopped?
|
||||
deployment.update_merge_request_metrics!
|
||||
if environment.save
|
||||
deployment.update_merge_request_metrics! unless environment.stopped?
|
||||
else
|
||||
# If there is a validation error on environment update, such as
|
||||
# the external URL is malformed, the error message is recorded for debugging purpose.
|
||||
# We should surface the error message to users for letting them to take an action.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/21182.
|
||||
Gitlab::ErrorTracking.track_exception(
|
||||
EnvironmentUpdateFailure.new,
|
||||
project_id: deployment.project_id,
|
||||
environment_id: environment.id,
|
||||
reason: environment.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
.form-group
|
||||
= f.label :email
|
||||
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
|
||||
.form-text.text-muted
|
||||
= _('Requires your primary GitLab email address.')
|
||||
.clearfix
|
||||
= f.submit _("Reset password"), class: "gl-button btn-confirm btn"
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
.flash-container
|
||||
%table.table.table-bordered
|
||||
%colgroup
|
||||
%col{ width: "30%" }
|
||||
%col{ width: "20%" }
|
||||
%col{ width: "20%" }
|
||||
%col{ width: "20%" }
|
||||
%col{ width: "20%" }
|
||||
%col{ width: "10%" }
|
||||
%col{ width: "10%" }
|
||||
- if can_admin_project
|
||||
%col
|
||||
%thead
|
||||
|
@ -23,8 +24,8 @@
|
|||
%th
|
||||
= s_("ProtectedBranch|Allowed to push")
|
||||
%th
|
||||
= s_("ProtectedBranch|Allow force push")
|
||||
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow force push for all users with push access.'), 'aria-hidden': 'true' }
|
||||
= s_("ProtectedBranch|Allowed to force push")
|
||||
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow all users with push access to force push.'), 'aria-hidden': 'true' }
|
||||
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-500')
|
||||
|
||||
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head'
|
||||
|
|
|
@ -22,11 +22,13 @@
|
|||
.col-md-10
|
||||
= yield :push_access_levels
|
||||
.form-group.row
|
||||
= f.label :allow_force_push, s_("ProtectedBranch|Allow force push:"), class: 'col-md-2 gl-text-left text-md-right'
|
||||
= f.label :allow_force_push, s_("ProtectedBranch|Allowed to force push:"), class: 'col-md-2 gl-text-left text-md-right'
|
||||
.col-md-10
|
||||
= render "shared/buttons/project_feature_toggle", class_list: "js-force-push-toggle project-feature-toggle"
|
||||
.form-text.gl-text-gray-600.gl-mt-0
|
||||
= s_("ProtectedBranch|Allow force push for all users with push access.")
|
||||
- force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push')
|
||||
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
|
||||
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
|
||||
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f
|
||||
.card-footer
|
||||
= f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' }
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
%ul
|
||||
%li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create new protected branches.
|
||||
%li Allow only users with Maintainer permissions to push code.
|
||||
%li Prevent <strong>anyone</strong> from force-pushing to the branch.
|
||||
%li Prevent <strong>anyone</strong> from #{link_to "force-pushing", help_page_path('topics/git/git_rebase', anchor: 'force-push')} to the branch.
|
||||
%li Prevent <strong>anyone</strong> from deleting the branch.
|
||||
|
||||
- if can? current_user, :admin_project, @project
|
||||
|
|
|
@ -34,4 +34,4 @@
|
|||
= _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
|
||||
|
||||
%td
|
||||
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allow force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }
|
||||
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allowed to force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_pending_builds_queue_join
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62195
|
||||
rollout_issue_url:
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_pending_builds_queue_maintain
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61581
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331496
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
|
|
|
@ -385,7 +385,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
|
||||
# The wiki and repository routing contains wildcard characters so
|
||||
# its preferable to keep it below all other project routes
|
||||
draw :repository_scoped
|
||||
draw :repository
|
||||
draw :wiki
|
||||
|
||||
namespace :import do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Repository routes without /-/ scope.
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/28848.
|
||||
# Do not add new routes here. Add new routes to repository_scoped.rb instead
|
||||
# Do not add new routes here. Add new routes to repository.rb instead
|
||||
# (see https://docs.gitlab.com/ee/development/routing.html#project-routes).
|
||||
|
||||
resource :repository, only: [:create]
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CleanUpPendingBuildsTable < ActiveRecord::Migration[6.0]
|
||||
BATCH_SIZE = 1000
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless Gitlab.dev_or_test_env? || Gitlab.com?
|
||||
|
||||
each_batch('ci_pending_builds', of: BATCH_SIZE) do |min, max|
|
||||
execute <<~SQL
|
||||
DELETE FROM ci_pending_builds
|
||||
USING ci_builds
|
||||
WHERE ci_builds.id = ci_pending_builds.build_id
|
||||
AND ci_builds.status != 'pending'
|
||||
AND ci_builds.type = 'Ci::Build'
|
||||
AND ci_pending_builds.id BETWEEN #{min} AND #{max}
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# noop
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def each_batch(table_name, scope: ->(table) { table.all }, of: 1000)
|
||||
table = Class.new(ActiveRecord::Base) do
|
||||
include EachBatch
|
||||
|
||||
self.table_name = table_name
|
||||
self.inheritance_column = :_type_disabled
|
||||
end
|
||||
|
||||
scope.call(table).each_batch(of: of) do |batch|
|
||||
yield batch.pluck('MIN(id), MAX(id)').first
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
5dc1119c5efe28225bb7ac8a9ed2c4c5cfaeaff202194ed4419cfd54eaf7483d
|
|
@ -1176,6 +1176,28 @@ Prints the metrics saved in `conversational_development_index_metrics`.
|
|||
rake gitlab:usage_data:generate_and_send
|
||||
```
|
||||
|
||||
## Kubernetes integration
|
||||
|
||||
Find cluster:
|
||||
|
||||
```ruby
|
||||
cluster = Clusters::Cluster.find(1)
|
||||
cluster = Clusters::Cluster.find_by(name: 'cluster_name')
|
||||
```
|
||||
|
||||
Delete cluster without associated resources:
|
||||
|
||||
```ruby
|
||||
# Find an admin user
|
||||
user = User.find_by(username: 'admin_user')
|
||||
|
||||
# Find the cluster with the ID
|
||||
cluster = Clusters::Cluster.find(1)
|
||||
|
||||
# Delete the cluster
|
||||
Clusters::DestroyService.new(user).execute(cluster)
|
||||
```
|
||||
|
||||
## Elasticsearch
|
||||
|
||||
### Configuration attributes
|
||||
|
|
|
@ -204,7 +204,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
|
|||
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
|
||||
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
|
||||
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
|
||||
| `allow_force_push` | boolean | no | Allow force push for all users with push access. (defaults: false) |
|
||||
| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
|
||||
| `allowed_to_push` | array | no | **(PREMIUM)** Array of access levels allowed to push, with each described by a hash |
|
||||
| `allowed_to_merge` | array | no | **(PREMIUM)** Array of access levels allowed to merge, with each described by a hash |
|
||||
| `allowed_to_unprotect` | array | no | **(PREMIUM)** Array of access levels allowed to unprotect, with each described by a hash |
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
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
|
||||
---
|
||||
|
||||
# Storybook
|
||||
|
||||
The Storybook for the `gitlab-org/gitlab` project is available on our [GitLab Pages site](https://gitlab-org.gitlab.io/gitlab/storybook).
|
||||
|
||||
## Storybook in local development
|
||||
|
||||
Storybook dependencies and configuration are located under the `storybook/` directory.
|
||||
|
||||
To build and launch Storybook locally, in the root directory of the `gitlab` project:
|
||||
|
||||
1. Install Storybook dependencies:
|
||||
|
||||
```shell
|
||||
yarn storybook:install
|
||||
```
|
||||
|
||||
1. Build the Storybook site:
|
||||
|
||||
```shell
|
||||
yarn storybook:start
|
||||
```
|
||||
|
||||
## Adding components to Storybook
|
||||
|
||||
Stories can be added for any Vue component in the `gitlab` repository.
|
||||
|
||||
To add a story:
|
||||
|
||||
1. Create a new `.stories.js` file in the same directory as the Vue component.
|
||||
The file name should have the same prefix as the Vue component.
|
||||
|
||||
```txt
|
||||
vue_shared/
|
||||
├─ components/
|
||||
│ ├─ todo_button.vue
|
||||
│ ├─ todo_button.stories.js
|
||||
```
|
||||
|
||||
1. Write the story as per the [official Storybook instructions](https://storybook.js.org/docs/vue/writing-stories/introduction)
|
||||
|
||||
Notes:
|
||||
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
|
||||
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/todo_button.vue`, specify the `title` as
|
||||
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.
|
|
@ -17,7 +17,7 @@ as the hardware requirements that are needed to install and use GitLab.
|
|||
- Ubuntu (16.04/18.04/20.04)
|
||||
- Debian (9/10)
|
||||
- CentOS (7/8)
|
||||
- openSUSE Leap (15.1/15.2)
|
||||
- openSUSE Leap (15.2)
|
||||
- SUSE Linux Enterprise Server (12 SP2/12 SP5)
|
||||
- Red Hat Enterprise Linux (please use the CentOS packages and instructions)
|
||||
- Scientific Linux (please use the CentOS packages and instructions)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
|
@ -184,17 +184,17 @@ command line or a Git client application.
|
|||
WARNING:
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
||||
You can allow force pushes to protected branches by either setting **Allow force push**
|
||||
You can allow [force pushes](../../topics/git/git_rebase.md#force-push) to
|
||||
protected branches by either setting **Allowed to force push**
|
||||
when you protect a new branch, or by configuring an already-protected branch.
|
||||
|
||||
To protect a new branch and enable Force push:
|
||||
|
||||
1. Navigate to your project's **Settings > Repository**.
|
||||
1. Expand **Protected branches**, and scroll to **Protect a branch**.
|
||||
![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v13_10.png)
|
||||
1. Select a **Branch** or wildcard you'd like to protect.
|
||||
1. Select the user levels **Allowed to merge** and **Allowed to push**.
|
||||
1. To allow all users with push access to force push, toggle the **Allow force push** slider.
|
||||
1. To allow all users with push access to force push, toggle the **Allowed to force push** slider.
|
||||
1. To reject code pushes that change files listed in the `CODEOWNERS` file, toggle
|
||||
**Require approval from code owners**.
|
||||
1. Click **Protect**.
|
||||
|
@ -203,8 +203,7 @@ To enable force pushes on branches already protected:
|
|||
|
||||
1. Navigate to your project's **Settings > Repository**.
|
||||
1. Expand **Protected branches** and scroll to **Protected branch**.
|
||||
![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v13_10.png)
|
||||
1. Toggle the **Allow force push** slider for the chosen branch.
|
||||
1. Toggle the **Allowed to force push** slider for the chosen branch.
|
||||
|
||||
When enabled, members who are allowed to push to this branch can also force push.
|
||||
|
||||
|
@ -224,15 +223,11 @@ To protect a new branch and enable Code Owner's approval:
|
|||
1. Scroll down to **Protect a branch**, select a **Branch** or wildcard you'd like to protect, select who's **Allowed to merge** and **Allowed to push**, and toggle the **Require approval from code owners** slider.
|
||||
1. Click **Protect**.
|
||||
|
||||
![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v13_10.png)
|
||||
|
||||
To enable Code Owner's approval to branches already protected:
|
||||
|
||||
1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
|
||||
1. Scroll down to **Protected branch** and toggle the **Code owner approval** slider for the chosen branch.
|
||||
|
||||
![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v13_10.png)
|
||||
|
||||
When enabled, all merge requests targeting these branches require approval
|
||||
by a Code Owner per matched rule before they can be merged.
|
||||
Additionally, direct pushes to the protected branch are denied if a rule is matched.
|
||||
|
|
|
@ -90,6 +90,7 @@ module API
|
|||
end
|
||||
get do
|
||||
authorize_read_feature_flag!
|
||||
exclude_legacy_flags_check!
|
||||
|
||||
present_entity(feature_flag)
|
||||
end
|
||||
|
@ -104,6 +105,7 @@ module API
|
|||
end
|
||||
post :enable do
|
||||
not_found! unless Feature.enabled?(:feature_flag_api, user_project)
|
||||
exclude_legacy_flags_check!
|
||||
render_api_error!('Version 2 flags not supported', :unprocessable_entity) if new_version_flag_present?
|
||||
|
||||
result = ::FeatureFlags::EnableService
|
||||
|
@ -127,6 +129,7 @@ module API
|
|||
end
|
||||
post :disable do
|
||||
not_found! unless Feature.enabled?(:feature_flag_api, user_project)
|
||||
exclude_legacy_flags_check!
|
||||
render_api_error!('Version 2 flags not supported', :unprocessable_entity) if feature_flag.new_version_flag?
|
||||
|
||||
result = ::FeatureFlags::DisableService
|
||||
|
@ -162,6 +165,7 @@ module API
|
|||
end
|
||||
put do
|
||||
authorize_update_feature_flag!
|
||||
exclude_legacy_flags_check!
|
||||
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag?
|
||||
|
||||
attrs = declared_params(include_missing: false)
|
||||
|
@ -232,6 +236,10 @@ module API
|
|||
@feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
|
||||
end
|
||||
|
||||
def project
|
||||
@project ||= feature_flag.project
|
||||
end
|
||||
|
||||
def new_version_flag_present?
|
||||
user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present?
|
||||
end
|
||||
|
@ -245,6 +253,14 @@ module API
|
|||
hash[key] = yield(hash[key]) if hash.key?(key)
|
||||
hash
|
||||
end
|
||||
|
||||
def exclude_legacy_flags_check!
|
||||
if Feature.enabled?(:remove_legacy_flags, project, default_enabled: :yaml) &&
|
||||
Feature.disabled?(:remove_legacy_flags_override, project, default_enabled: :yaml) &&
|
||||
feature_flag.legacy_flag?
|
||||
not_found!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26651,13 +26651,16 @@ msgstr ""
|
|||
msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Allow force push"
|
||||
msgid "ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Allow force push for all users with push access."
|
||||
msgid "ProtectedBranch|Allow all users with push access to force push."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Allow force push:"
|
||||
msgid "ProtectedBranch|Allowed to force push"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Allowed to force push:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Allowed to merge"
|
||||
|
@ -26702,7 +26705,7 @@ msgstr ""
|
|||
msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Toggle allow force push"
|
||||
msgid "ProtectedBranch|Toggle allowed to force push"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedBranch|Toggle code owner approval"
|
||||
|
@ -27987,6 +27990,9 @@ msgstr[1] ""
|
|||
msgid "Requires values to meet regular expression requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "Requires your primary GitLab email address."
|
||||
msgstr ""
|
||||
|
||||
msgid "Resend"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
"markdownlint:no-trailing-spaces": "markdownlint --config doc/.markdownlint/markdownlint-no-trailing-spaces.yml",
|
||||
"markdownlint:no-trailing-spaces:fix": "yarn run markdownlint:no-trailing-spaces --fix",
|
||||
"postinstall": "node ./scripts/frontend/postinstall.js",
|
||||
"storybook:install": "yarn --cwd ./storybook install",
|
||||
"storybook:start": "yarn --cwd ./storybook start",
|
||||
"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",
|
||||
|
|
|
@ -6,7 +6,7 @@ module QA
|
|||
RSpec.describe 'Create' do
|
||||
context 'Gitaly' do
|
||||
# Issue to track removal of feature flag: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/602
|
||||
describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin do
|
||||
describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/322814', type: :investigating } do
|
||||
let(:number_of_reads_per_loop) { 9 }
|
||||
let(:praefect_manager) { Service::PraefectManager.new }
|
||||
let(:project) do
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'parallel'
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do
|
||||
context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/331989', type: :investigating } do
|
||||
let(:praefect_manager) { Service::PraefectManager.new }
|
||||
let(:project) do
|
||||
Resource::Project.fabricate! do |project|
|
||||
|
|
|
@ -371,6 +371,58 @@ RSpec.describe Projects::FeatureFlagsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET edit' do
|
||||
subject { get(:edit, params: params) }
|
||||
|
||||
context 'with legacy flags' do
|
||||
let!(:feature_flag) { create(:operations_feature_flag, project: project) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
iid: feature_flag.iid
|
||||
}
|
||||
end
|
||||
|
||||
context 'removed' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
is_expected.to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'removed' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: false)
|
||||
end
|
||||
|
||||
it 'returns ok' do
|
||||
is_expected.to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with new version flags' do
|
||||
let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
iid: feature_flag.iid
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns successfully' do
|
||||
is_expected.to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST create.json' do
|
||||
subject { post(:create, params: params, format: :json) }
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ FactoryBot.define do
|
|||
|
||||
trait :pending do
|
||||
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
|
||||
|
||||
status { 'pending' }
|
||||
end
|
||||
|
||||
|
@ -286,6 +287,15 @@ FactoryBot.define do
|
|||
|
||||
trait :queued do
|
||||
queued_at { Time.now }
|
||||
|
||||
after(:create) do |build|
|
||||
build.create_queuing_entry!
|
||||
end
|
||||
end
|
||||
|
||||
trait :picked do
|
||||
running
|
||||
|
||||
runner factory: :ci_runner
|
||||
end
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.value') do
|
||||
page.within('[data-testid="select-milestone"]') do
|
||||
expect(page).to have_content(milestone.title)
|
||||
end
|
||||
end
|
||||
|
@ -56,7 +56,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
|
|||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.value') do
|
||||
page.within('[data-testid="select-milestone"]') do
|
||||
expect(page).not_to have_content(milestone.title)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { GlDrawer } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
|
||||
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
||||
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||
import { ISSUABLE } from '~/boards/constants';
|
||||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||
|
@ -68,6 +68,9 @@ describe('BoardContentSidebar', () => {
|
|||
iterations: {
|
||||
loading: false,
|
||||
},
|
||||
attributesList: {
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -84,38 +87,41 @@ describe('BoardContentSidebar', () => {
|
|||
});
|
||||
|
||||
it('confirms we render GlDrawer', () => {
|
||||
expect(wrapper.find(GlDrawer).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render GlDrawer when isSidebarOpen is false', () => {
|
||||
createStore({ mockGetters: { isSidebarOpen: () => false } });
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find(GlDrawer).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('applies an open attribute', () => {
|
||||
expect(wrapper.find(GlDrawer).props('open')).toBe(true);
|
||||
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarLabelsSelect', () => {
|
||||
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarTitle', () => {
|
||||
expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarDueDate', () => {
|
||||
expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarSubscription', () => {
|
||||
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarMilestoneSelect', () => {
|
||||
expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true);
|
||||
it('renders SidebarDropdownWidget for milestones', () => {
|
||||
expect(wrapper.findComponent(SidebarDropdownWidget).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SidebarDropdownWidget).props('issuableAttribute')).toEqual(
|
||||
'milestone',
|
||||
);
|
||||
});
|
||||
|
||||
describe('when we emit close', () => {
|
||||
|
@ -128,7 +134,7 @@ describe('BoardContentSidebar', () => {
|
|||
});
|
||||
|
||||
it('calls toggleBoardItem with correct parameters', async () => {
|
||||
wrapper.find(GlDrawer).vm.$emit('close');
|
||||
wrapper.findComponent(GlDrawer).vm.$emit('close');
|
||||
|
||||
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
|
||||
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
|
||||
|
|
|
@ -56,6 +56,18 @@ describe('noteable_discussion component', () => {
|
|||
expect(wrapper.find('.discussion-header').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide actions when diff refs do not exists', async () => {
|
||||
const discussion = { ...discussionMock };
|
||||
discussion.diff_file = { ...mockDiffFile, diff_refs: null };
|
||||
discussion.diff_discussion = true;
|
||||
discussion.expanded = false;
|
||||
|
||||
wrapper.setProps({ discussion });
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.canShowReplyActions).toBe(false);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('should toggle reply form', async () => {
|
||||
await nextTick();
|
||||
|
|
|
@ -0,0 +1,503 @@
|
|||
import {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownText,
|
||||
GlLink,
|
||||
GlSearchBoxByType,
|
||||
GlFormInput,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
|
||||
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
|
||||
import { IssuableAttributeType } from '~/sidebar/constants';
|
||||
import projectIssueMilestoneMutation from '~/sidebar/queries/project_issue_milestone.mutation.graphql';
|
||||
import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
|
||||
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
|
||||
|
||||
import {
|
||||
mockIssue,
|
||||
mockProjectMilestonesResponse,
|
||||
noCurrentMilestoneResponse,
|
||||
mockMilestoneMutationResponse,
|
||||
mockMilestone2,
|
||||
emptyProjectMilestonesResponse,
|
||||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
describe('SidebarDropdownWidget', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
|
||||
const firstErrorMsg = 'first error';
|
||||
const promiseWithErrors = {
|
||||
...promiseData,
|
||||
issuableSetAttribute: { ...promiseData.issuableSetAttribute, errors: [firstErrorMsg] },
|
||||
};
|
||||
|
||||
const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
|
||||
const mutationError = () =>
|
||||
jest.fn().mockRejectedValue('Failed to set milestone on this issue. Please try again.');
|
||||
const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors });
|
||||
|
||||
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDropdownText = () => wrapper.findComponent(GlDropdownText);
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findDropdownItemWithText = (text) =>
|
||||
findAllDropdownItems().wrappers.find((x) => x.text() === text);
|
||||
|
||||
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
|
||||
const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
|
||||
const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon);
|
||||
const findAttributeItems = () => wrapper.findByTestId('milestone-items');
|
||||
const findSelectedAttribute = () => wrapper.findByTestId('select-milestone');
|
||||
const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
|
||||
const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
|
||||
|
||||
const waitForDropdown = async () => {
|
||||
// BDropdown first changes its `visible` property
|
||||
// in a requestAnimationFrame callback.
|
||||
// It then emits `shown` event in a watcher for `visible`
|
||||
// Hence we need both of these:
|
||||
await waitForPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
};
|
||||
|
||||
const waitForApollo = async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
// Used with createComponentWithApollo which uses 'mount'
|
||||
const clickEdit = async () => {
|
||||
await findEditButton().trigger('click');
|
||||
|
||||
await waitForDropdown();
|
||||
|
||||
// We should wait for attributes list to be fetched.
|
||||
await waitForApollo();
|
||||
};
|
||||
|
||||
// Used with createComponent which shallow mounts components
|
||||
const toggleDropdown = async () => {
|
||||
wrapper.vm.$refs.editable.expand();
|
||||
|
||||
await waitForDropdown();
|
||||
};
|
||||
|
||||
const createComponentWithApollo = async ({
|
||||
requestHandlers = [],
|
||||
projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
|
||||
currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
|
||||
} = {}) => {
|
||||
localVue.use(VueApollo);
|
||||
mockApollo = createMockApollo([
|
||||
[projectMilestonesQuery, projectMilestonesSpy],
|
||||
[projectIssueMilestoneQuery, currentMilestoneSpy],
|
||||
...requestHandlers,
|
||||
]);
|
||||
|
||||
wrapper = extendedWrapper(
|
||||
mount(SidebarDropdownWidget, {
|
||||
localVue,
|
||||
provide: { canUpdate: true },
|
||||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
workspacePath: mockIssue.projectPath,
|
||||
attrWorkspacePath: mockIssue.projectPath,
|
||||
iid: mockIssue.iid,
|
||||
issuableType: IssuableType.Issue,
|
||||
issuableAttribute: IssuableAttributeType.Milestone,
|
||||
},
|
||||
attachTo: document.body,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitForApollo();
|
||||
};
|
||||
|
||||
const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(SidebarDropdownWidget, {
|
||||
provide: { canUpdate: true },
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
propsData: {
|
||||
workspacePath: '',
|
||||
attrWorkspacePath: '',
|
||||
iid: '',
|
||||
issuableType: IssuableType.Issue,
|
||||
issuableAttribute: IssuableAttributeType.Milestone,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
mutate: mutationPromise(),
|
||||
queries: {
|
||||
currentAttribute: { loading: false },
|
||||
attributesList: { loading: false },
|
||||
...queries,
|
||||
},
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
SidebarEditableItem,
|
||||
GlSearchBoxByType,
|
||||
GlDropdown,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// We need to mock out `showDropdown` which
|
||||
// invokes `show` method of BDropdown used inside GlDropdown.
|
||||
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('when not editing', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
data: {
|
||||
currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl' },
|
||||
},
|
||||
stubs: {
|
||||
GlDropdown,
|
||||
SidebarEditableItem,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the current attribute', () => {
|
||||
expect(findSelectedAttribute().text()).toBe('title');
|
||||
});
|
||||
|
||||
it('links to the current attribute', () => {
|
||||
expect(findGlLink().attributes().href).toBe('webUrl');
|
||||
});
|
||||
|
||||
it('does not show a loading spinner next to the heading', () => {
|
||||
expect(findEditableLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows a loading spinner while fetching the current attribute', () => {
|
||||
createComponent({
|
||||
queries: {
|
||||
currentAttribute: { loading: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findEditableLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the loading spinner and the title of the selected attribute while updating', () => {
|
||||
createComponent({
|
||||
data: {
|
||||
updating: true,
|
||||
selectedTitle: 'Some milestone title',
|
||||
},
|
||||
queries: {
|
||||
currentAttribute: { loading: false },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findEditableLoadingIcon().exists()).toBe(true);
|
||||
expect(findSelectedAttribute().text()).toBe('Some milestone title');
|
||||
});
|
||||
|
||||
describe('when current attribute does not exist', () => {
|
||||
it('renders "None" as the selected attribute title', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSelectedAttribute().text()).toBe('None');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user can edit', () => {
|
||||
describe('when user is editing', () => {
|
||||
describe('when rendering the dropdown', () => {
|
||||
it('shows a loading spinner while fetching a list of attributes', async () => {
|
||||
createComponent({
|
||||
queries: {
|
||||
attributesList: { loading: true },
|
||||
},
|
||||
});
|
||||
|
||||
await toggleDropdown();
|
||||
|
||||
expect(findLoadingIconDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('GlDropdownItem with the right title and id', () => {
|
||||
const id = 'id';
|
||||
const title = 'title';
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
data: { attributesList: [{ id, title }], currentAttribute: { id, title } },
|
||||
});
|
||||
|
||||
await toggleDropdown();
|
||||
});
|
||||
|
||||
it('does not show a loading spinner', () => {
|
||||
expect(findLoadingIconDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders title $title', () => {
|
||||
expect(findDropdownItemWithText(title).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('checks the correct dropdown item', () => {
|
||||
expect(
|
||||
findAllDropdownItems()
|
||||
.filter((w) => w.props('isChecked') === true)
|
||||
.at(0)
|
||||
.text(),
|
||||
).toBe(title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no data is assigned', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
await toggleDropdown();
|
||||
});
|
||||
|
||||
it('finds GlDropdownItem with "No milestone"', () => {
|
||||
expect(findNoAttributeItem().text()).toBe('No milestone');
|
||||
});
|
||||
|
||||
it('"No milestone" is checked', () => {
|
||||
expect(findNoAttributeItem().props('isChecked')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render any dropdown item', () => {
|
||||
expect(findAttributeItems().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clicking on dropdown item', () => {
|
||||
describe('when currentAttribute is equal to attribute id', () => {
|
||||
it('does not call setIssueAttribute mutation', async () => {
|
||||
createComponent({
|
||||
data: {
|
||||
attributesList: [{ id: 'id', title: 'title' }],
|
||||
currentAttribute: { id: 'id', title: 'title' },
|
||||
},
|
||||
});
|
||||
|
||||
await toggleDropdown();
|
||||
|
||||
findDropdownItemWithText('title').vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when currentAttribute is not equal to attribute id', () => {
|
||||
describe('when error', () => {
|
||||
const bootstrapComponent = (mutationResp) => {
|
||||
createComponent({
|
||||
data: {
|
||||
attributesList: [
|
||||
{ id: '123', title: '123' },
|
||||
{ id: 'id', title: 'title' },
|
||||
],
|
||||
currentAttribute: '123',
|
||||
},
|
||||
mutationPromise: mutationResp,
|
||||
});
|
||||
};
|
||||
|
||||
describe.each`
|
||||
description | mutationResp | expectedMsg
|
||||
${'top-level error'} | ${mutationError} | ${'Failed to set milestone on this issue. Please try again.'}
|
||||
${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg}
|
||||
`(`$description`, ({ mutationResp, expectedMsg }) => {
|
||||
beforeEach(async () => {
|
||||
bootstrapComponent(mutationResp);
|
||||
|
||||
await toggleDropdown();
|
||||
|
||||
findDropdownItemWithText('title').vm.$emit('click');
|
||||
});
|
||||
|
||||
it(`calls createFlash with "${expectedMsg}"`, async () => {
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: expectedMsg,
|
||||
captureError: true,
|
||||
error: expectedMsg,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user is searching', () => {
|
||||
describe('when search result is not found', () => {
|
||||
it('renders "No milestone found"', async () => {
|
||||
createComponent();
|
||||
|
||||
await toggleDropdown();
|
||||
|
||||
findSearchBox().vm.$emit('input', 'non existing milestones');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDropdownText().text()).toBe('No milestone found');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mock apollo', () => {
|
||||
let error;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Sentry, 'captureException');
|
||||
error = new Error('mayday');
|
||||
});
|
||||
|
||||
describe("when issuable type is 'issue'", () => {
|
||||
describe('when dropdown is expanded and user can edit', () => {
|
||||
let milestoneMutationSpy;
|
||||
beforeEach(async () => {
|
||||
milestoneMutationSpy = jest.fn().mockResolvedValue(mockMilestoneMutationResponse);
|
||||
|
||||
await createComponentWithApollo({
|
||||
requestHandlers: [[projectIssueMilestoneMutation, milestoneMutationSpy]],
|
||||
});
|
||||
|
||||
await clickEdit();
|
||||
});
|
||||
|
||||
it('renders the dropdown on clicking edit', async () => {
|
||||
expect(findDropdown().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('focuses on the input when dropdown is shown', async () => {
|
||||
expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
|
||||
});
|
||||
|
||||
describe('when currentAttribute is not equal to attribute id', () => {
|
||||
describe('when update is successful', () => {
|
||||
beforeEach(() => {
|
||||
findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
|
||||
});
|
||||
|
||||
it('calls setIssueAttribute mutation', () => {
|
||||
expect(milestoneMutationSpy).toHaveBeenCalledWith({
|
||||
iid: mockIssue.iid,
|
||||
attributeId: getIdFromGraphQLId(mockMilestone2.id),
|
||||
fullPath: mockIssue.projectPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the value returned from the mutation to currentAttribute', async () => {
|
||||
expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('milestones', () => {
|
||||
let projectMilestonesSpy;
|
||||
|
||||
it('should call createFlash if milestones query fails', async () => {
|
||||
await createComponentWithApollo({
|
||||
projectMilestonesSpy: jest.fn().mockRejectedValue(error),
|
||||
});
|
||||
|
||||
await clickEdit();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: wrapper.vm.i18n.listFetchError,
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
it('only fetches attributes when dropdown is opened', async () => {
|
||||
projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse);
|
||||
await createComponentWithApollo({ projectMilestonesSpy });
|
||||
|
||||
expect(projectMilestonesSpy).not.toHaveBeenCalled();
|
||||
|
||||
await clickEdit();
|
||||
|
||||
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
|
||||
fullPath: mockIssue.projectPath,
|
||||
title: '',
|
||||
state: 'active',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user is searching', () => {
|
||||
const mockSearchTerm = 'foobar';
|
||||
|
||||
beforeEach(async () => {
|
||||
projectMilestonesSpy = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(emptyProjectMilestonesResponse);
|
||||
await createComponentWithApollo({ projectMilestonesSpy });
|
||||
|
||||
await clickEdit();
|
||||
});
|
||||
|
||||
it('sends a projectMilestones query with the entered search term "foo"', async () => {
|
||||
findSearchBox().vm.$emit('input', mockSearchTerm);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Account for debouncing
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
|
||||
fullPath: mockIssue.projectPath,
|
||||
title: mockSearchTerm,
|
||||
state: 'active',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentAttributes', () => {
|
||||
it('should call createFlash if currentAttributes query fails', async () => {
|
||||
await createComponentWithApollo({
|
||||
currentMilestoneSpy: jest.fn().mockRejectedValue(error),
|
||||
});
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: wrapper.vm.i18n.currentFetchError,
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -513,4 +513,83 @@ export const participantsQueryResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockGroupPath = 'gitlab-org';
|
||||
export const mockProjectPath = `${mockGroupPath}/some-project`;
|
||||
|
||||
export const mockIssue = {
|
||||
projectPath: mockProjectPath,
|
||||
iid: '1',
|
||||
groupPath: mockGroupPath,
|
||||
};
|
||||
|
||||
export const mockIssueId = 'gid://gitlab/Issue/1';
|
||||
|
||||
export const mockMilestone1 = {
|
||||
__typename: 'Milestone',
|
||||
id: 'gid://gitlab/Milestone/1',
|
||||
title: 'Foobar Milestone',
|
||||
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
|
||||
state: 'active',
|
||||
};
|
||||
|
||||
export const mockMilestone2 = {
|
||||
__typename: 'Milestone',
|
||||
id: 'gid://gitlab/Milestone/2',
|
||||
title: 'Awesome Milestone',
|
||||
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
|
||||
state: 'active',
|
||||
};
|
||||
|
||||
export const mockProjectMilestonesResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
attributes: {
|
||||
nodes: [mockMilestone1, mockMilestone2],
|
||||
},
|
||||
__typename: 'MilestoneConnection',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
};
|
||||
|
||||
export const noCurrentMilestoneResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
issuable: { id: mockIssueId, attribute: null, __typename: 'Issue' },
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockMilestoneMutationResponse = {
|
||||
data: {
|
||||
issuableSetAttribute: {
|
||||
errors: [],
|
||||
issuable: {
|
||||
id: 'gid://gitlab/Issue/1',
|
||||
attribute: {
|
||||
id: 'gid://gitlab/Milestone/2',
|
||||
title: 'Awesome Milestone',
|
||||
state: 'active',
|
||||
__typename: 'Milestone',
|
||||
},
|
||||
__typename: 'Issue',
|
||||
},
|
||||
__typename: 'UpdateIssuePayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyProjectMilestonesResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
attributes: {
|
||||
nodes: [],
|
||||
},
|
||||
__typename: 'MilestoneConnection',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20210525075724_clean_up_pending_builds_table.rb')
|
||||
|
||||
RSpec.describe CleanUpPendingBuildsTable do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:queue) { table(:ci_pending_builds) }
|
||||
let(:builds) { table(:ci_builds) }
|
||||
|
||||
before do
|
||||
namespaces.create!(id: 123, name: 'sample', path: 'sample')
|
||||
projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123)
|
||||
|
||||
builds.create!(id: 1, project_id: 123, status: 'pending', type: 'Ci::Build')
|
||||
builds.create!(id: 2, project_id: 123, status: 'pending', type: 'GenericCommitStatus')
|
||||
builds.create!(id: 3, project_id: 123, status: 'success', type: 'Ci::Bridge')
|
||||
builds.create!(id: 4, project_id: 123, status: 'success', type: 'Ci::Build')
|
||||
builds.create!(id: 5, project_id: 123, status: 'running', type: 'Ci::Build')
|
||||
builds.create!(id: 6, project_id: 123, status: 'created', type: 'Ci::Build')
|
||||
|
||||
queue.create!(id: 1, project_id: 123, build_id: 1)
|
||||
queue.create!(id: 2, project_id: 123, build_id: 4)
|
||||
queue.create!(id: 3, project_id: 123, build_id: 5)
|
||||
end
|
||||
|
||||
it 'removes duplicated data from pending builds table' do
|
||||
migrate!
|
||||
|
||||
expect(queue.all.count).to eq 1
|
||||
expect(queue.first.id).to eq 1
|
||||
expect(builds.all.count).to eq 6
|
||||
end
|
||||
|
||||
context 'when there are multiple batches' do
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 1)
|
||||
end
|
||||
|
||||
it 'iterates the data correctly' do
|
||||
migrate!
|
||||
|
||||
expect(queue.all.count).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
|
@ -354,7 +354,7 @@ RSpec.describe Ci::Build do
|
|||
it 'does not push build to the queue' do
|
||||
build.enqueue
|
||||
|
||||
expect(::Ci::PendingBuild.all.count).to be_zero
|
||||
expect(build.queuing_entry).not_to be_present
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
let(:runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
let(:user) { create(:user) }
|
||||
let(:job) do
|
||||
create(:ci_build, :artifacts, :extended_options,
|
||||
create(:ci_build, :pending, :queued, :artifacts, :extended_options,
|
||||
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
|
||||
end
|
||||
|
||||
|
@ -129,7 +129,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
context 'when other projects have pending jobs' do
|
||||
before do
|
||||
job.success
|
||||
create(:ci_build, :pending)
|
||||
create(:ci_build, :pending, :queued)
|
||||
end
|
||||
|
||||
it_behaves_like 'no jobs available'
|
||||
|
@ -239,7 +239,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when job is made for tag' do
|
||||
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
|
||||
it 'sets branch as ref_type' do
|
||||
request_job
|
||||
|
@ -297,7 +297,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when job filtered by job_age' do
|
||||
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
|
||||
|
||||
context 'job is queued less than job_age parameter' do
|
||||
let(:job_age) { 120 }
|
||||
|
@ -359,7 +359,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when job is for a release' do
|
||||
let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :release_options, pipeline: pipeline) }
|
||||
|
||||
context 'when `multi_build_steps` is passed by the runner' do
|
||||
it 'exposes release info' do
|
||||
|
@ -398,7 +398,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
|
||||
context 'when job is made for merge request' do
|
||||
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
|
||||
let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
||||
it 'sets branch as ref_type' do
|
||||
|
@ -479,9 +479,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when project and pipeline have multiple jobs' do
|
||||
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
|
||||
|
||||
before do
|
||||
job.success
|
||||
|
@ -531,8 +531,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when pipeline have jobs with artifacts' do
|
||||
let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
|
||||
|
||||
before do
|
||||
job.success
|
||||
|
@ -551,10 +551,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when explicit dependencies are defined' do
|
||||
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:test_job) do
|
||||
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
|
||||
stage: 'deploy', stage_idx: 1,
|
||||
options: { script: ['bash'], dependencies: [job2.name] })
|
||||
end
|
||||
|
@ -575,10 +575,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
context 'when dependencies is an empty array' do
|
||||
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
|
||||
let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
|
||||
let!(:empty_dependencies_job) do
|
||||
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
|
||||
stage: 'deploy', stage_idx: 1,
|
||||
options: { script: ['bash'], dependencies: [] })
|
||||
end
|
||||
|
@ -739,7 +739,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
|
||||
describe 'port support' do
|
||||
let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
|
||||
let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
|
||||
|
||||
context 'when job image has ports' do
|
||||
let(:options) do
|
||||
|
@ -791,7 +791,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
describe 'a job with excluded artifacts' do
|
||||
context 'when excluded paths are defined' do
|
||||
let(:job) do
|
||||
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test',
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test',
|
||||
stage: 'deploy', stage_idx: 1,
|
||||
options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
|
||||
end
|
||||
|
@ -839,7 +839,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
subject { request_job }
|
||||
|
||||
context 'when triggered by a user' do
|
||||
let(:job) { create(:ci_build, user: user, project: project) }
|
||||
let(:job) { create(:ci_build, :pending, :queued, user: user, project: project) }
|
||||
|
||||
subject { request_job(id: job.id) }
|
||||
|
||||
|
|
|
@ -148,6 +148,18 @@ RSpec.describe API::FeatureFlags do
|
|||
expect(json_response['version']).to eq('legacy_flag')
|
||||
end
|
||||
|
||||
context 'without legacy flags' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'check user permission'
|
||||
end
|
||||
|
||||
|
@ -492,6 +504,18 @@ RSpec.describe API::FeatureFlags do
|
|||
end
|
||||
|
||||
it_behaves_like 'check user permission'
|
||||
|
||||
context 'without legacy flags' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag exists already' do
|
||||
|
@ -537,6 +561,18 @@ RSpec.describe API::FeatureFlags do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without legacy flags' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a version 2 flag' do
|
||||
|
@ -612,6 +648,18 @@ RSpec.describe API::FeatureFlags do
|
|||
})
|
||||
end
|
||||
|
||||
context 'without legacy flags' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'check user permission'
|
||||
|
||||
context 'when strategies become empty array after the removal' do
|
||||
|
@ -976,6 +1024,20 @@ RSpec.describe API::FeatureFlags do
|
|||
expect(feature_flag.reload.strategies.first.scopes.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without legacy flags' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
|
||||
end
|
||||
|
||||
it 'returns not found' do
|
||||
params = { description: 'new description' }
|
||||
|
||||
put api("/projects/#{project.id}/feature_flags/other_flag_name", user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/feature_flags/:name' do
|
||||
|
|
|
@ -11,7 +11,13 @@ RSpec.describe MergeRequestDiffEntity do
|
|||
let(:merge_request_diff) { merge_request_diffs.first }
|
||||
|
||||
let(:entity) do
|
||||
described_class.new(merge_request_diff, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
|
||||
described_class.new(
|
||||
merge_request_diff,
|
||||
request: request,
|
||||
merge_request: merge_request,
|
||||
merge_request_diff: merge_request_diff,
|
||||
merge_request_diffs: merge_request_diffs
|
||||
)
|
||||
end
|
||||
|
||||
subject { entity.as_json }
|
||||
|
@ -26,6 +32,46 @@ RSpec.describe MergeRequestDiffEntity do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#version_index' do
|
||||
shared_examples 'version_index is nil' do
|
||||
it 'returns nil' do
|
||||
expect(subject[:version_index]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff is not present' do
|
||||
let(:entity) do
|
||||
described_class.new(
|
||||
merge_request_diff,
|
||||
request: request,
|
||||
merge_request: merge_request,
|
||||
merge_request_diffs: merge_request_diffs
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'version_index is nil'
|
||||
end
|
||||
|
||||
context 'when diff is not included in @merge_request_diffs' do
|
||||
let(:merge_request_diff) { create(:merge_request_diff) }
|
||||
let(:merge_request_diff_2) { create(:merge_request_diff) }
|
||||
|
||||
before do
|
||||
merge_request_diffs << merge_request_diff_2
|
||||
end
|
||||
|
||||
it_behaves_like 'version_index is nil'
|
||||
end
|
||||
|
||||
context 'when @merge_request_diffs.size <= 1' do
|
||||
before do
|
||||
expect(merge_request_diffs.size).to eq(1)
|
||||
end
|
||||
|
||||
it_behaves_like 'version_index is nil'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#short_commit_sha' do
|
||||
it 'returns short sha' do
|
||||
expect(subject[:short_commit_sha]).to eq('b83d6e39')
|
||||
|
|
|
@ -11,7 +11,7 @@ module Ci
|
|||
let!(:shared_runner) { create(:ci_runner, :instance) }
|
||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'checks database loadbalancing stickiness' do
|
||||
|
@ -104,11 +104,11 @@ module Ci
|
|||
let!(:project3) { create :project, shared_runners_enabled: true }
|
||||
let!(:pipeline3) { create :ci_pipeline, project: project3 }
|
||||
let!(:build1_project1) { pending_job }
|
||||
let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
|
||||
let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
|
||||
let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
|
||||
let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
|
||||
let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 }
|
||||
let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
|
||||
let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
|
||||
let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
|
||||
|
||||
context 'when using fair scheduling' do
|
||||
context 'when all builds are pending' do
|
||||
|
@ -255,17 +255,17 @@ module Ci
|
|||
let!(:pipeline3) { create(:ci_pipeline, project: project3) }
|
||||
|
||||
let!(:build1_project1) { pending_job }
|
||||
let!(:build2_project1) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:build3_project1) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) }
|
||||
let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) }
|
||||
let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) }
|
||||
let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
|
||||
let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
|
||||
let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
|
||||
let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
|
||||
let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
|
||||
|
||||
# these shouldn't influence the scheduling
|
||||
let!(:unrelated_group) { create(:group) }
|
||||
let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
|
||||
let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
|
||||
let!(:build1_unrelated_project) { create(:ci_build, pipeline: unrelated_pipeline) }
|
||||
let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
|
||||
let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
|
||||
|
||||
it 'does not consider builds from other group runners' do
|
||||
|
@ -346,7 +346,7 @@ module Ci
|
|||
subject { described_class.new(specific_runner).execute }
|
||||
|
||||
context 'with multiple builds are in queue' do
|
||||
let!(:other_build) { create :ci_build, pipeline: pipeline }
|
||||
let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
|
||||
|
@ -387,7 +387,7 @@ module Ci
|
|||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
|
||||
context 'when a job is protected' do
|
||||
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
|
||||
|
||||
it 'picks the job' do
|
||||
expect(execute(specific_runner)).to eq(pending_job)
|
||||
|
@ -395,7 +395,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when a job is unprotected' do
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
it 'picks the job' do
|
||||
expect(execute(specific_runner)).to eq(pending_job)
|
||||
|
@ -403,7 +403,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when protected attribute of a job is nil' do
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
pending_job.update_attribute(:protected, nil)
|
||||
|
@ -419,7 +419,7 @@ module Ci
|
|||
let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
|
||||
|
||||
context 'when a job is protected' do
|
||||
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
|
||||
|
||||
it 'picks the job' do
|
||||
expect(execute(specific_runner)).to eq(pending_job)
|
||||
|
@ -427,7 +427,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when a job is unprotected' do
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
it 'does not pick the job' do
|
||||
expect(execute(specific_runner)).to be_nil
|
||||
|
@ -435,7 +435,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when protected attribute of a job is nil' do
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
pending_job.update_attribute(:protected, nil)
|
||||
|
@ -449,7 +449,7 @@ module Ci
|
|||
|
||||
context 'runner feature set is verified' do
|
||||
let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
|
||||
let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, options: options) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
|
||||
|
||||
subject { execute(specific_runner, params) }
|
||||
|
||||
|
@ -485,7 +485,7 @@ module Ci
|
|||
|
||||
shared_examples 'validation is active' do
|
||||
context 'when depended job has not been completed yet' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
it { expect(subject).to eq(pending_job) }
|
||||
end
|
||||
|
@ -522,7 +522,7 @@ module Ci
|
|||
|
||||
shared_examples 'validation is not active' do
|
||||
context 'when depended job has not been completed yet' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
it { expect(subject).to eq(pending_job) }
|
||||
end
|
||||
|
@ -547,7 +547,7 @@ module Ci
|
|||
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
let!(:pending_job) do
|
||||
create(:ci_build, :pending,
|
||||
create(:ci_build, :pending, :queued,
|
||||
pipeline: pipeline, stage_idx: 1,
|
||||
options: { script: ["bash"], dependencies: ['test'] })
|
||||
end
|
||||
|
@ -558,7 +558,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when build is degenerated' do
|
||||
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
|
||||
|
||||
subject { execute(specific_runner, {}) }
|
||||
|
||||
|
@ -573,7 +573,7 @@ module Ci
|
|||
|
||||
context 'when build has data integrity problem' do
|
||||
let!(:pending_job) do
|
||||
create(:ci_build, :pending, pipeline: pipeline)
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -598,7 +598,7 @@ module Ci
|
|||
|
||||
context 'when build fails to be run!' do
|
||||
let!(:pending_job) do
|
||||
create(:ci_build, :pending, pipeline: pipeline)
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -640,12 +640,12 @@ module Ci
|
|||
|
||||
context 'when only some builds can be matched by runner' do
|
||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline, tag_list: %w[matching]) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
|
||||
|
||||
before do
|
||||
# create additional matching and non-matching jobs
|
||||
create_list(:ci_build, 2, pipeline: pipeline, tag_list: %w[matching])
|
||||
create(:ci_build, pipeline: pipeline, tag_list: %w[non-matching])
|
||||
create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
|
||||
create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
|
||||
end
|
||||
|
||||
it 'observes queue size of only matching jobs' do
|
||||
|
@ -693,7 +693,7 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when there is another build in queue' do
|
||||
let!(:next_pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
it 'skips this build and picks another build' do
|
||||
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
|
||||
|
@ -732,6 +732,22 @@ module Ci
|
|||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
|
||||
context 'when joining with pending builds table' do
|
||||
before do
|
||||
stub_feature_flags(ci_pending_builds_queue_join: true)
|
||||
end
|
||||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
|
||||
context 'when not joining with pending builds table' do
|
||||
before do
|
||||
stub_feature_flags(ci_pending_builds_queue_join: false)
|
||||
end
|
||||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#register_success' do
|
||||
|
@ -775,8 +791,8 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when project already has running jobs' do
|
||||
let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
|
||||
let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
|
||||
let!(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
|
||||
let!(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
|
||||
|
||||
it 'counts job queuing time histogram with expected labels' do
|
||||
allow(attempt_counter).to receive(:increment)
|
||||
|
@ -859,9 +875,9 @@ module Ci
|
|||
end
|
||||
|
||||
context 'when max queue depth is reached' do
|
||||
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
|
||||
let!(:pending_job_2) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
|
||||
let!(:pending_job_3) { create(:ci_build, :pending, pipeline: pipeline) }
|
||||
let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
|
||||
let!(:pending_job_2) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
|
||||
let!(:pending_job_3) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2)
|
||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe Ci::RetryBuildService do
|
|||
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
|
||||
|
||||
let_it_be(:build) do
|
||||
create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags,
|
||||
create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
|
||||
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
|
||||
description: 'my-job', stage: 'test', stage_id: stage.id,
|
||||
pipeline: pipeline, auto_canceled_by: another_pipeline,
|
||||
|
|
|
@ -95,6 +95,42 @@ RSpec.describe Deployments::UpdateEnvironmentService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when external URL is specified and the tier is unset' do
|
||||
let(:options) { { name: 'production', url: external_url } }
|
||||
|
||||
before do
|
||||
environment.update_columns(external_url: external_url, tier: nil)
|
||||
job.update!(environment: 'production')
|
||||
end
|
||||
|
||||
context 'when external URL is valid' do
|
||||
let(:external_url) { 'https://google.com' }
|
||||
|
||||
it 'succeeds to update the tier automatically' do
|
||||
expect { subject.execute }.to change { environment.tier }.from(nil).to('production')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when external URL is invalid' do
|
||||
let(:external_url) { 'google.com' }
|
||||
|
||||
it 'fails to update the tier due to validation error' do
|
||||
expect { subject.execute }.not_to change { environment.tier }
|
||||
end
|
||||
|
||||
it 'tracks an exception' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
.with(an_instance_of(described_class::EnvironmentUpdateFailure),
|
||||
project_id: project.id,
|
||||
environment_id: environment.id,
|
||||
reason: %q{External url is blocked: Only allowed schemes are http, https})
|
||||
.once
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when variables are used' do
|
||||
let(:options) do
|
||||
{ name: 'review-apps/$CI_COMMIT_REF_NAME',
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
public/
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable import/no-commonjs */
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../../app/assets/javascripts/**/*.stories.js',
|
||||
'../../ee/app/assets/javascripts/**/*.stories.js',
|
||||
],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
const stylesheetsRequireCtx = require.context(
|
||||
'../../app/assets/stylesheets',
|
||||
true,
|
||||
/application\.scss$/,
|
||||
);
|
||||
|
||||
stylesheetsRequireCtx('./application.scss');
|
|
@ -0,0 +1,103 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
const { statSync } = require('fs');
|
||||
const path = require('path');
|
||||
const sass = require('node-sass'); // eslint-disable-line import/no-unresolved
|
||||
const { buildIncludePaths, resolveGlobUrl } = require('node-sass-magic-importer/dist/toolbox'); // eslint-disable-line import/no-unresolved
|
||||
const webpack = require('webpack');
|
||||
const gitlabWebpackConfig = require('../../config/webpack.config.js');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../../');
|
||||
const TRANSPARENT_1X1_PNG =
|
||||
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)';
|
||||
const SASS_INCLUDE_PATHS = [
|
||||
'app/assets/stylesheets',
|
||||
'ee/app/assets/stylesheets',
|
||||
'ee/app/assets/stylesheets/_ee',
|
||||
'node_modules',
|
||||
].map((p) => path.resolve(ROOT, p));
|
||||
|
||||
/**
|
||||
* Custom importer for node-sass, used when LibSass encounters the `@import` directive.
|
||||
* Doc source: https://github.com/sass/node-sass#importer--v200---experimental
|
||||
* @param {*} url the path in import as-is, which LibSass encountered.
|
||||
* @param {*} prev the previously resolved path.
|
||||
* @returns {Object | null} the new import string.
|
||||
*/
|
||||
function sassSmartImporter(url, prev) {
|
||||
const nodeSassOptions = this.options;
|
||||
const includePaths = buildIncludePaths(nodeSassOptions.includePaths, prev).filter(
|
||||
(includePath) => !includePath.includes('node_modules'),
|
||||
);
|
||||
|
||||
// GitLab extensively uses glob-style import paths, but
|
||||
// Sass doesn't support glob-style URLs out of the box.
|
||||
// Here, we try and resolve the glob URL.
|
||||
// If it resolves, we update the @import statement with the resolved path.
|
||||
const filePaths = resolveGlobUrl(url, includePaths);
|
||||
if (filePaths) {
|
||||
const contents = filePaths
|
||||
.filter((file) => statSync(file).isFile())
|
||||
.map((x) => `@import '${x}';`)
|
||||
.join(`\n`);
|
||||
|
||||
return { contents };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const sassLoaderOptions = {
|
||||
functions: {
|
||||
'image-url($url)': function sassImageUrlStub() {
|
||||
return new sass.types.String(TRANSPARENT_1X1_PNG);
|
||||
},
|
||||
'asset_path($url)': function sassAssetPathStub() {
|
||||
return new sass.types.String(TRANSPARENT_1X1_PNG);
|
||||
},
|
||||
'asset_url($url)': function sassAssetUrlStub() {
|
||||
return new sass.types.String(TRANSPARENT_1X1_PNG);
|
||||
},
|
||||
'url($url)': function sassUrlStub() {
|
||||
return new sass.types.String(TRANSPARENT_1X1_PNG);
|
||||
},
|
||||
},
|
||||
includePaths: SASS_INCLUDE_PATHS,
|
||||
importer: sassSmartImporter,
|
||||
};
|
||||
|
||||
module.exports = function storybookWebpackConfig({ config }) {
|
||||
// Add any missing extensions from the main GitLab webpack config
|
||||
config.resolve.extensions = Array.from(
|
||||
new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]),
|
||||
);
|
||||
|
||||
// Replace any Storybook-defined CSS loaders with our custom one.
|
||||
config.module.rules = [
|
||||
...config.module.rules.filter((r) => !r.test.test('.css')),
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
exclude: /typescale\/\w+_demo\.scss$/, // skip typescale demo stylesheets
|
||||
loaders: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: sassLoaderOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Silence webpack warnings about moment/pikaday not being able to resolve.
|
||||
config.plugins.push(new webpack.IgnorePlugin(/moment/, /pikaday/));
|
||||
|
||||
// Add any missing aliases from the main GitLab webpack config
|
||||
Object.assign(config.resolve.alias, gitlabWebpackConfig.resolve.alias);
|
||||
// The main GitLab project aliases this `icons.svg` file to app/assets/javascripts/lib/utils/icons_path.js,
|
||||
// which depends on the existence of a global `gon` variable.
|
||||
// By deleting the alias, imports of this path will resolve as expected.
|
||||
delete config.resolve.alias['@gitlab/svgs/dist/icons.svg'];
|
||||
|
||||
return config;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "start-storybook -p 9002 -c config",
|
||||
"build": "build-storybook -c config -o public"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-a11y": "^6.2.9",
|
||||
"@storybook/addon-actions": "^6.2.9",
|
||||
"@storybook/addon-controls": "^6.2.9",
|
||||
"@storybook/addon-essentials": "^6.2.9",
|
||||
"@storybook/vue": "6.2.9",
|
||||
"node-sass": "^4.14.1",
|
||||
"node-sass-magic-importer": "^5.3.2",
|
||||
"postcss-loader": "3.0.0",
|
||||
"sass-loader": "^7.1.0"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue