Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2195019331
commit
9c191c0b94
34 changed files with 516 additions and 214 deletions
|
@ -2,6 +2,15 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 14.2.3 (2021-09-01)
|
||||||
|
|
||||||
|
### Fixed (4 changes)
|
||||||
|
|
||||||
|
- [Fix Live Markdown Preview in personal and subgroup projects](gitlab-org/gitlab@20553f93703c0bc076c8e1a4fbc4ce07e2e914b7) ([merge request](gitlab-org/gitlab!69316))
|
||||||
|
- [Fix OrphanedInviteTokensCleanup migration](gitlab-org/gitlab@9c59b2fbdfeb250de66a9d2b9424cde9680f86c3) ([merge request](gitlab-org/gitlab!69316))
|
||||||
|
- [Reset severity_levels default](gitlab-org/gitlab@34e65788679cfbdeec28357a01a8b303ba61418f) ([merge request](gitlab-org/gitlab!69316))
|
||||||
|
- [Geo: Replicate multi-arch containers](gitlab-org/gitlab@fdf88767320016a84c83e896b9f9b90291de89e0) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67624)) **GitLab Enterprise Edition**
|
||||||
|
|
||||||
## 14.2.2 (2021-08-31)
|
## 14.2.2 (2021-08-31)
|
||||||
|
|
||||||
### Security (9 changes)
|
### Security (9 changes)
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButtonGroup, GlButton } from '@gitlab/ui';
|
import { GlButtonGroup, GlButton } from '@gitlab/ui';
|
||||||
|
|
||||||
const SCALE_STEP_SIZE = 0.2;
|
|
||||||
const DEFAULT_SCALE = 1;
|
const DEFAULT_SCALE = 1;
|
||||||
const MIN_SCALE = 1;
|
const MIN_SCALE = 1;
|
||||||
const MAX_SCALE = 2;
|
const ZOOM_LEVELS = 5;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlButtonGroup,
|
GlButtonGroup,
|
||||||
GlButton,
|
GlButton,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
maxScale: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
scale: DEFAULT_SCALE,
|
scale: DEFAULT_SCALE,
|
||||||
|
@ -24,7 +29,10 @@ export default {
|
||||||
return this.scale === DEFAULT_SCALE;
|
return this.scale === DEFAULT_SCALE;
|
||||||
},
|
},
|
||||||
disableIncrease() {
|
disableIncrease() {
|
||||||
return this.scale >= MAX_SCALE;
|
return this.scale >= this.maxScale;
|
||||||
|
},
|
||||||
|
stepSize() {
|
||||||
|
return (this.maxScale - MIN_SCALE) / ZOOM_LEVELS;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -37,10 +45,10 @@ export default {
|
||||||
this.$emit('scale', this.scale);
|
this.$emit('scale', this.scale);
|
||||||
},
|
},
|
||||||
incrementScale() {
|
incrementScale() {
|
||||||
this.setScale(this.scale + SCALE_STEP_SIZE);
|
this.setScale(Math.min(this.scale + this.stepSize, this.maxScale));
|
||||||
},
|
},
|
||||||
decrementScale() {
|
decrementScale() {
|
||||||
this.setScale(this.scale - SCALE_STEP_SIZE);
|
this.setScale(Math.max(this.scale - this.stepSize, MIN_SCALE));
|
||||||
},
|
},
|
||||||
resetScale() {
|
resetScale() {
|
||||||
this.setScale(DEFAULT_SCALE);
|
this.setScale(DEFAULT_SCALE);
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
onImgLoad() {
|
onImgLoad() {
|
||||||
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
|
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
|
||||||
|
requestIdleCallback(this.setImageNaturalScale, { timeout: 1000 });
|
||||||
performanceMarkAndMeasure({
|
performanceMarkAndMeasure({
|
||||||
measures: [
|
measures: [
|
||||||
{
|
{
|
||||||
|
@ -79,6 +80,27 @@ export default {
|
||||||
};
|
};
|
||||||
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
|
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
|
||||||
},
|
},
|
||||||
|
setImageNaturalScale() {
|
||||||
|
const { contentImg } = this.$refs;
|
||||||
|
|
||||||
|
if (!contentImg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { naturalHeight, naturalWidth } = contentImg;
|
||||||
|
|
||||||
|
// In case image 404s
|
||||||
|
if (naturalHeight === 0 || naturalWidth === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { height, width } = this.baseImageSize;
|
||||||
|
|
||||||
|
this.$parent.$emit(
|
||||||
|
'setMaxScale',
|
||||||
|
Math.round(((height + width) / (naturalHeight + naturalWidth)) * 100) / 100,
|
||||||
|
);
|
||||||
|
},
|
||||||
onResize({ width, height }) {
|
onResize({ width, height }) {
|
||||||
this.$emit('resize', { width, height });
|
this.$emit('resize', { width, height });
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,7 @@ import {
|
||||||
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
|
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
|
||||||
|
|
||||||
const DEFAULT_SCALE = 1;
|
const DEFAULT_SCALE = 1;
|
||||||
|
const DEFAULT_MAX_SCALE = 2;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -96,6 +97,7 @@ export default {
|
||||||
scale: DEFAULT_SCALE,
|
scale: DEFAULT_SCALE,
|
||||||
resolvedDiscussionsExpanded: false,
|
resolvedDiscussionsExpanded: false,
|
||||||
prevCurrentUserTodos: null,
|
prevCurrentUserTodos: null,
|
||||||
|
maxScale: DEFAULT_MAX_SCALE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -328,6 +330,9 @@ export default {
|
||||||
toggleResolvedComments() {
|
toggleResolvedComments() {
|
||||||
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
|
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
|
||||||
},
|
},
|
||||||
|
setMaxScale(event) {
|
||||||
|
this.maxScale = 1 / event;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createImageDiffNoteMutation,
|
createImageDiffNoteMutation,
|
||||||
DESIGNS_ROUTE_NAME,
|
DESIGNS_ROUTE_NAME,
|
||||||
|
@ -376,12 +381,13 @@ export default {
|
||||||
@openCommentForm="openCommentForm"
|
@openCommentForm="openCommentForm"
|
||||||
@closeCommentForm="closeCommentForm"
|
@closeCommentForm="closeCommentForm"
|
||||||
@moveNote="onMoveNote"
|
@moveNote="onMoveNote"
|
||||||
|
@setMaxScale="setMaxScale"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
||||||
>
|
>
|
||||||
<design-scaler @scale="scale = $event" />
|
<design-scaler :max-scale="maxScale" @scale="scale = $event" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<design-sidebar
|
<design-sidebar
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
GlTooltipDirective,
|
GlTooltipDirective,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||||
|
import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { TYPE_USER } from '~/graphql_shared/constants';
|
import { TYPE_USER } from '~/graphql_shared/constants';
|
||||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
@ -21,7 +21,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
|
||||||
import {
|
import {
|
||||||
CREATED_DESC,
|
CREATED_DESC,
|
||||||
i18n,
|
i18n,
|
||||||
issuesCountSmartQueryBase,
|
|
||||||
MAX_LIST_SIZE,
|
MAX_LIST_SIZE,
|
||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
PARAM_DUE_DATE,
|
PARAM_DUE_DATE,
|
||||||
|
@ -164,18 +163,16 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const filterTokens = getFilterTokens(window.location.search);
|
|
||||||
const state = getParameterByName(PARAM_STATE);
|
const state = getParameterByName(PARAM_STATE);
|
||||||
const sortKey = getSortKey(getParameterByName(PARAM_SORT));
|
const sortKey = getSortKey(getParameterByName(PARAM_SORT));
|
||||||
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
|
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
|
||||||
|
|
||||||
this.initialFilterTokens = cloneDeep(filterTokens);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
|
||||||
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
|
||||||
filterTokens,
|
filterTokens: getFilterTokens(window.location.search),
|
||||||
issues: [],
|
issues: [],
|
||||||
|
issuesCounts: {},
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
pageParams: getInitialPageParams(sortKey),
|
pageParams: getInitialPageParams(sortKey),
|
||||||
showBulkEditSidebar: false,
|
showBulkEditSidebar: false,
|
||||||
|
@ -202,40 +199,21 @@ export default {
|
||||||
},
|
},
|
||||||
debounce: 200,
|
debounce: 200,
|
||||||
},
|
},
|
||||||
countOpened: {
|
issuesCounts: {
|
||||||
...issuesCountSmartQueryBase,
|
query: getIssuesCountsQuery,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return this.queryVariables;
|
||||||
...this.queryVariables,
|
},
|
||||||
state: IssuableStates.Opened,
|
update: ({ project }) => project ?? {},
|
||||||
};
|
error(error) {
|
||||||
|
createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.hasAnyIssues;
|
return !this.hasAnyIssues;
|
||||||
},
|
},
|
||||||
},
|
debounce: 200,
|
||||||
countClosed: {
|
context: {
|
||||||
...issuesCountSmartQueryBase,
|
isSingleRequest: true,
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
...this.queryVariables,
|
|
||||||
state: IssuableStates.Closed,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
skip() {
|
|
||||||
return !this.hasAnyIssues;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
countAll: {
|
|
||||||
...issuesCountSmartQueryBase,
|
|
||||||
variables() {
|
|
||||||
return {
|
|
||||||
...this.queryVariables,
|
|
||||||
state: IssuableStates.All,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
skip() {
|
|
||||||
return !this.hasAnyIssues;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -263,6 +241,9 @@ export default {
|
||||||
isOpenTab() {
|
isOpenTab() {
|
||||||
return this.state === IssuableStates.Opened;
|
return this.state === IssuableStates.Opened;
|
||||||
},
|
},
|
||||||
|
showCsvButtons() {
|
||||||
|
return this.isSignedIn;
|
||||||
|
},
|
||||||
apiFilterParams() {
|
apiFilterParams() {
|
||||||
return convertToApiParams(this.filterTokens);
|
return convertToApiParams(this.filterTokens);
|
||||||
},
|
},
|
||||||
|
@ -405,10 +386,11 @@ export default {
|
||||||
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
|
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
|
||||||
},
|
},
|
||||||
tabCounts() {
|
tabCounts() {
|
||||||
|
const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
|
||||||
return {
|
return {
|
||||||
[IssuableStates.Opened]: this.countOpened,
|
[IssuableStates.Opened]: openedIssues?.count,
|
||||||
[IssuableStates.Closed]: this.countClosed,
|
[IssuableStates.Closed]: closedIssues?.count,
|
||||||
[IssuableStates.All]: this.countAll,
|
[IssuableStates.All]: allIssues?.count,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
currentTabCount() {
|
currentTabCount() {
|
||||||
|
@ -584,13 +566,13 @@ export default {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const serializedVariables = JSON.stringify(this.queryVariables);
|
const serializedVariables = JSON.stringify(this.queryVariables);
|
||||||
this.$apollo.mutate({
|
return this.$apollo.mutate({
|
||||||
mutation: reorderIssuesMutation,
|
mutation: reorderIssuesMutation,
|
||||||
variables: { oldIndex, newIndex, serializedVariables },
|
variables: { oldIndex, newIndex, serializedVariables },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
createFlash({ message: this.$options.i18n.reorderError });
|
createFlash({ message: this.$options.i18n.reorderError, captureError: true, error });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSort(sortKey) {
|
handleSort(sortKey) {
|
||||||
|
@ -613,7 +595,7 @@ export default {
|
||||||
recent-searches-storage-key="issues"
|
recent-searches-storage-key="issues"
|
||||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||||
:search-tokens="searchTokens"
|
:search-tokens="searchTokens"
|
||||||
:initial-filter-value="initialFilterTokens"
|
:initial-filter-value="filterTokens"
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
:initial-sort-by="sortKey"
|
:initial-sort-by="sortKey"
|
||||||
:issuables="issues"
|
:issuables="issues"
|
||||||
|
@ -653,7 +635,7 @@ export default {
|
||||||
:aria-label="$options.i18n.calendarLabel"
|
:aria-label="$options.i18n.calendarLabel"
|
||||||
/>
|
/>
|
||||||
<csv-import-export-buttons
|
<csv-import-export-buttons
|
||||||
v-if="isSignedIn"
|
v-if="showCsvButtons"
|
||||||
class="gl-md-mr-3"
|
class="gl-md-mr-3"
|
||||||
:export-csv-path="exportCsvPathWithQuery"
|
:export-csv-path="exportCsvPathWithQuery"
|
||||||
:issuable-count="currentTabCount"
|
:issuable-count="currentTabCount"
|
||||||
|
@ -766,6 +748,7 @@ export default {
|
||||||
{{ $options.i18n.newIssueLabel }}
|
{{ $options.i18n.newIssueLabel }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<csv-import-export-buttons
|
<csv-import-export-buttons
|
||||||
|
v-if="showCsvButtons"
|
||||||
class="gl-mr-3"
|
class="gl-mr-3"
|
||||||
:export-csv-path="exportCsvPathWithQuery"
|
:export-csv-path="exportCsvPathWithQuery"
|
||||||
:issuable-count="currentTabCount"
|
:issuable-count="currentTabCount"
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
|
|
||||||
import createFlash from '~/flash';
|
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import {
|
import {
|
||||||
FILTER_ANY,
|
FILTER_ANY,
|
||||||
|
@ -351,15 +349,3 @@ export const filters = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const issuesCountSmartQueryBase = {
|
|
||||||
query: getIssuesCountQuery,
|
|
||||||
context: {
|
|
||||||
isSingleRequest: true,
|
|
||||||
},
|
|
||||||
update: ({ project }) => project?.issues.count,
|
|
||||||
error(error) {
|
|
||||||
createFlash({ message: i18n.errorFetchingCounts, captureError: true, error });
|
|
||||||
},
|
|
||||||
debounce: 200,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
query getIssuesCount(
|
|
||||||
$fullPath: ID!
|
|
||||||
$search: String
|
|
||||||
$state: IssuableState
|
|
||||||
$assigneeId: String
|
|
||||||
$assigneeUsernames: [String!]
|
|
||||||
$authorUsername: String
|
|
||||||
$labelName: [String]
|
|
||||||
$milestoneTitle: [String]
|
|
||||||
$milestoneWildcardId: MilestoneWildcardId
|
|
||||||
$types: [IssueType!]
|
|
||||||
$not: NegatedIssueFilterInput
|
|
||||||
) {
|
|
||||||
project(fullPath: $fullPath) {
|
|
||||||
issues(
|
|
||||||
search: $search
|
|
||||||
state: $state
|
|
||||||
assigneeId: $assigneeId
|
|
||||||
assigneeUsernames: $assigneeUsernames
|
|
||||||
authorUsername: $authorUsername
|
|
||||||
labelName: $labelName
|
|
||||||
milestoneTitle: $milestoneTitle
|
|
||||||
milestoneWildcardId: $milestoneWildcardId
|
|
||||||
types: $types
|
|
||||||
not: $not
|
|
||||||
) {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
query getIssuesCount(
|
||||||
|
$fullPath: ID!
|
||||||
|
$search: String
|
||||||
|
$assigneeId: String
|
||||||
|
$assigneeUsernames: [String!]
|
||||||
|
$authorUsername: String
|
||||||
|
$labelName: [String]
|
||||||
|
$milestoneTitle: [String]
|
||||||
|
$milestoneWildcardId: MilestoneWildcardId
|
||||||
|
$types: [IssueType!]
|
||||||
|
$not: NegatedIssueFilterInput
|
||||||
|
) {
|
||||||
|
project(fullPath: $fullPath) {
|
||||||
|
openedIssues: issues(
|
||||||
|
state: opened
|
||||||
|
search: $search
|
||||||
|
assigneeId: $assigneeId
|
||||||
|
assigneeUsernames: $assigneeUsernames
|
||||||
|
authorUsername: $authorUsername
|
||||||
|
labelName: $labelName
|
||||||
|
milestoneTitle: $milestoneTitle
|
||||||
|
milestoneWildcardId: $milestoneWildcardId
|
||||||
|
types: $types
|
||||||
|
not: $not
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
closedIssues: issues(
|
||||||
|
state: closed
|
||||||
|
search: $search
|
||||||
|
assigneeId: $assigneeId
|
||||||
|
assigneeUsernames: $assigneeUsernames
|
||||||
|
authorUsername: $authorUsername
|
||||||
|
labelName: $labelName
|
||||||
|
milestoneTitle: $milestoneTitle
|
||||||
|
milestoneWildcardId: $milestoneWildcardId
|
||||||
|
types: $types
|
||||||
|
not: $not
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
allIssues: issues(
|
||||||
|
state: all
|
||||||
|
search: $search
|
||||||
|
assigneeId: $assigneeId
|
||||||
|
assigneeUsernames: $assigneeUsernames
|
||||||
|
authorUsername: $authorUsername
|
||||||
|
labelName: $labelName
|
||||||
|
milestoneTitle: $milestoneTitle
|
||||||
|
milestoneWildcardId: $milestoneWildcardId
|
||||||
|
types: $types
|
||||||
|
not: $not
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
module IssuablesHelper
|
module IssuablesHelper
|
||||||
include GitlabRoutingHelper
|
include GitlabRoutingHelper
|
||||||
include IssuablesDescriptionTemplatesHelper
|
include IssuablesDescriptionTemplatesHelper
|
||||||
|
include ::Sidebars::Concerns::HasPill
|
||||||
|
|
||||||
def sidebar_gutter_toggle_icon
|
def sidebar_gutter_toggle_icon
|
||||||
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
|
content_tag(:span, class: 'js-sidebar-toggle-container', data: { is_expanded: !sidebar_gutter_collapsed? }) do
|
||||||
|
@ -187,19 +188,18 @@ module IssuablesHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def issuables_state_counter_text(issuable_type, state, display_count)
|
def issuables_state_counter_text(issuable_type, state, display_count)
|
||||||
titles = {
|
titles = { opened: "Open" }
|
||||||
opened: "Open"
|
|
||||||
}
|
|
||||||
|
|
||||||
state_title = titles[state] || state.to_s.humanize
|
state_title = titles[state] || state.to_s.humanize
|
||||||
html = content_tag(:span, state_title)
|
html = content_tag(:span, state_title)
|
||||||
|
|
||||||
return html.html_safe unless display_count
|
return html.html_safe unless display_count
|
||||||
|
|
||||||
count = issuables_count_for_state(issuable_type, state)
|
count = issuables_count_for_state(issuable_type, state)
|
||||||
|
|
||||||
if count != -1
|
if count != -1
|
||||||
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm')
|
html << " " << content_tag(:span,
|
||||||
|
format_count(issuable_type, count, Gitlab::IssuablesCountForState::THRESHOLD),
|
||||||
|
class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm'
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
html.html_safe
|
html.html_safe
|
||||||
|
@ -284,7 +284,9 @@ module IssuablesHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def issuables_count_for_state(issuable_type, state)
|
def issuables_count_for_state(issuable_type, state)
|
||||||
Gitlab::IssuablesCountForState.new(finder)[state]
|
store_in_cache = parent.is_a?(Group) ? parent.cached_issues_state_count_enabled? : false
|
||||||
|
|
||||||
|
Gitlab::IssuablesCountForState.new(finder, store_in_redis_cache: store_in_cache)[state]
|
||||||
end
|
end
|
||||||
|
|
||||||
def close_issuable_path(issuable)
|
def close_issuable_path(issuable)
|
||||||
|
@ -438,6 +440,14 @@ module IssuablesHelper
|
||||||
def parent
|
def parent
|
||||||
@project || @group
|
@project || @group
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_count(issuable_type, count, threshold)
|
||||||
|
if issuable_type == :issues && parent.is_a?(Group) && parent.cached_issues_state_count_enabled?
|
||||||
|
format_cached_count(threshold, count)
|
||||||
|
else
|
||||||
|
number_with_delimiter(count)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
IssuablesHelper.prepend_mod_with('IssuablesHelper')
|
IssuablesHelper.prepend_mod_with('IssuablesHelper')
|
||||||
|
|
|
@ -735,6 +735,10 @@ class Group < Namespace
|
||||||
Timelog.in_group(self)
|
Timelog.in_group(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cached_issues_state_count_enabled?
|
||||||
|
Feature.enabled?(:cached_issues_state_count, self, default_enabled: :yaml)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def max_member_access(user_ids)
|
def max_member_access(user_ids)
|
||||||
|
|
|
@ -2117,9 +2117,12 @@ class User < ApplicationRecord
|
||||||
project_creation_levels << nil
|
project_creation_levels << nil
|
||||||
end
|
end
|
||||||
|
|
||||||
developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
|
if Feature.enabled?(:linear_user_groups_with_developer_maintainer_project_access, self, default_enabled: :yaml)
|
||||||
::Group.where(id: developer_groups_hierarchy.select(:id),
|
developer_groups.self_and_descendants.where(project_creation_level: project_creation_levels)
|
||||||
project_creation_level: project_creation_levels)
|
else
|
||||||
|
developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
|
||||||
|
::Group.where(id: developer_groups_hierarchy.select(:id), project_creation_level: project_creation_levels)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def no_recent_activity?
|
def no_recent_activity?
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: cached_issues_state_count
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67418
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333089
|
||||||
|
milestone: '14.3'
|
||||||
|
type: development
|
||||||
|
group: group::product planning
|
||||||
|
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: linear_user_groups_with_developer_maintainer_project_access
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68851
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339436
|
||||||
|
milestone: '14.3'
|
||||||
|
type: development
|
||||||
|
group: group::access
|
||||||
|
default_enabled: false
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
data_category: operational
|
|
||||||
key_path: usage_activity_by_stage_monthly.secure.sast_scans
|
|
||||||
description: ''
|
|
||||||
product_section: ''
|
|
||||||
product_stage: ''
|
|
||||||
product_group: ''
|
|
||||||
product_category: ''
|
|
||||||
value_type: number
|
|
||||||
status: data_available
|
|
||||||
time_frame: 28d
|
|
||||||
data_source:
|
|
||||||
distribution:
|
|
||||||
- ce
|
|
||||||
tier:
|
|
||||||
- free
|
|
||||||
skip_validation: true
|
|
||||||
performance_indicator_type: []
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
data_category: operational
|
|
||||||
key_path: usage_activity_by_stage_monthly.secure.container_scanning_scans
|
|
||||||
description: ''
|
|
||||||
product_section: ''
|
|
||||||
product_stage: ''
|
|
||||||
product_group: ''
|
|
||||||
product_category: ''
|
|
||||||
value_type: number
|
|
||||||
status: data_available
|
|
||||||
time_frame: 28d
|
|
||||||
data_source:
|
|
||||||
distribution:
|
|
||||||
- ce
|
|
||||||
tier:
|
|
||||||
- free
|
|
||||||
skip_validation: true
|
|
||||||
performance_indicator_type: []
|
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
data_category: operational
|
|
||||||
key_path: usage_activity_by_stage_monthly.secure.secret_detection_scans
|
|
||||||
description: ''
|
|
||||||
product_section: ''
|
|
||||||
product_stage: ''
|
|
||||||
product_group: ''
|
|
||||||
product_category: ''
|
|
||||||
value_type: number
|
|
||||||
status: data_available
|
|
||||||
time_frame: 28d
|
|
||||||
data_source:
|
|
||||||
distribution:
|
|
||||||
- ce
|
|
||||||
tier:
|
|
||||||
- free
|
|
||||||
skip_validation: true
|
|
||||||
performance_indicator_type: []
|
|
|
@ -24,10 +24,9 @@ tests access to the GitLab API.
|
||||||
GITLAB_ROOT_PASSWORD: "password" # to access the api with user root:password
|
GITLAB_ROOT_PASSWORD: "password" # to access the api with user root:password
|
||||||
```
|
```
|
||||||
|
|
||||||
1. To set values for the `GITLAB_HTTPS` and `GITLAB_ROOT_PASSWORD`,
|
NOTE:
|
||||||
[assign them to a variable in the user interface](../variables/index.md#add-a-cicd-variable-to-a-project).
|
Variables set in the GitLab UI are not passed down to the service containers.
|
||||||
Then assign that variable to the corresponding variable in your
|
[Learn more](../variables/index.md#).
|
||||||
`.gitlab-ci.yml` file.
|
|
||||||
|
|
||||||
Then, commands in `script:` sections in your `.gitlab-ci.yml` file can access the API at `http://gitlab/api/v4`.
|
Then, commands in `script:` sections in your `.gitlab-ci.yml` file can access the API at `http://gitlab/api/v4`.
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,9 @@ If you want to use a MySQL container, you can use [GitLab Runner](../runners/ind
|
||||||
|
|
||||||
This example shows you how to set a username and password that GitLab uses to access the MySQL container. If you do not set a username and password, you must use `root`.
|
This example shows you how to set a username and password that GitLab uses to access the MySQL container. If you do not set a username and password, you must use `root`.
|
||||||
|
|
||||||
1. [Create CI/CD variables](../variables/index.md#custom-cicd-variables) for your
|
NOTE:
|
||||||
MySQL database and password by going to **Settings > CI/CD**, expanding **Variables**,
|
Variables set in the GitLab UI are not passed down to the service containers.
|
||||||
and clicking **Add Variable**.
|
[Learn more](../variables/index.md).
|
||||||
|
|
||||||
This example uses `$MYSQL_DB` and `$MYSQL_PASS` as the keys.
|
|
||||||
|
|
||||||
1. To specify a MySQL image, add the following to your `.gitlab-ci.yml` file:
|
1. To specify a MySQL image, add the following to your `.gitlab-ci.yml` file:
|
||||||
|
|
||||||
|
@ -39,8 +37,8 @@ This example shows you how to set a username and password that GitLab uses to ac
|
||||||
```yaml
|
```yaml
|
||||||
variables:
|
variables:
|
||||||
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
|
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
|
||||||
MYSQL_DATABASE: $MYSQL_DB
|
MYSQL_DATABASE: $MYSQL_DATABASE
|
||||||
MYSQL_ROOT_PASSWORD: $MYSQL_PASS
|
MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
|
||||||
```
|
```
|
||||||
|
|
||||||
The MySQL container uses `MYSQL_DATABASE` and `MYSQL_ROOT_PASSWORD` to connect to the database.
|
The MySQL container uses `MYSQL_DATABASE` and `MYSQL_ROOT_PASSWORD` to connect to the database.
|
||||||
|
|
|
@ -16,6 +16,10 @@ do this with the Docker and Shell executors of GitLab Runner.
|
||||||
If you're using [GitLab Runner](../runners/index.md) with the Docker executor,
|
If you're using [GitLab Runner](../runners/index.md) with the Docker executor,
|
||||||
you basically have everything set up already.
|
you basically have everything set up already.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
Variables set in the GitLab UI are not passed down to the service containers.
|
||||||
|
[Learn more](../variables/index.md).
|
||||||
|
|
||||||
First, in your `.gitlab-ci.yml` add:
|
First, in your `.gitlab-ci.yml` add:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -23,25 +27,19 @@ services:
|
||||||
- postgres:12.2-alpine
|
- postgres:12.2-alpine
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
POSTGRES_DB: nice_marmot
|
POSTGRES_DB: $POSTGRES_DB
|
||||||
POSTGRES_USER: runner
|
POSTGRES_USER: $POSTGRES_USER
|
||||||
POSTGRES_PASSWORD: ""
|
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
```
|
```
|
||||||
|
|
||||||
To set values for the `POSTGRES_DB`, `POSTGRES_USER`,
|
|
||||||
`POSTGRES_PASSWORD` and `POSTGRES_HOST_AUTH_METHOD`,
|
|
||||||
[assign them to a CI/CD variable in the user interface](../variables/index.md#custom-cicd-variables),
|
|
||||||
then assign that variable to the corresponding variable in your
|
|
||||||
`.gitlab-ci.yml` file.
|
|
||||||
|
|
||||||
And then configure your application to use the database, for example:
|
And then configure your application to use the database, for example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Host: postgres
|
Host: postgres
|
||||||
User: runner
|
User: $PG_USER
|
||||||
Password: ''
|
Password: $PG_PASSWORD
|
||||||
Database: nice_marmot
|
Database: $PG_DB
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're wondering why we used `postgres` for the `Host`, read more at
|
If you're wondering why we used `postgres` for the `Host`, read more at
|
||||||
|
|
|
@ -20,6 +20,15 @@ You can use [predefined CI/CD variables](#predefined-cicd-variables) or define c
|
||||||
- [Group CI/CD variables](#add-a-cicd-variable-to-a-group).
|
- [Group CI/CD variables](#add-a-cicd-variable-to-a-group).
|
||||||
- [Instance CI/CD variables](#add-a-cicd-variable-to-an-instance).
|
- [Instance CI/CD variables](#add-a-cicd-variable-to-an-instance).
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
Variables set in the GitLab UI are **not** passed down to [service containers](../docker/using_docker_images.md).
|
||||||
|
To set them, assign them to variables in the UI, then re-assign them in your `.gitlab-ci.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
variables:
|
||||||
|
SA_PASSWORD: $SA_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
> For more information about advanced use of GitLab CI/CD:
|
> For more information about advanced use of GitLab CI/CD:
|
||||||
>
|
>
|
||||||
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Get to productivity faster with these [7 advanced GitLab CI workflow hacks](https://about.gitlab.com/webcast/7cicd-hacks/)
|
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Get to productivity faster with these [7 advanced GitLab CI workflow hacks](https://about.gitlab.com/webcast/7cicd-hacks/)
|
||||||
|
|
|
@ -5,11 +5,14 @@ module Gitlab
|
||||||
class IssuablesCountForState
|
class IssuablesCountForState
|
||||||
# The name of the Gitlab::SafeRequestStore cache key.
|
# The name of the Gitlab::SafeRequestStore cache key.
|
||||||
CACHE_KEY = :issuables_count_for_state
|
CACHE_KEY = :issuables_count_for_state
|
||||||
|
# The expiration time for the Rails cache.
|
||||||
|
CACHE_EXPIRES_IN = 10.minutes
|
||||||
|
THRESHOLD = 1000
|
||||||
|
|
||||||
# The state values that can be safely casted to a Symbol.
|
# The state values that can be safely casted to a Symbol.
|
||||||
STATES = %w[opened closed merged all].freeze
|
STATES = %w[opened closed merged all].freeze
|
||||||
|
|
||||||
attr_reader :project
|
attr_reader :project, :finder
|
||||||
|
|
||||||
def self.declarative_policy_class
|
def self.declarative_policy_class
|
||||||
'IssuablePolicy'
|
'IssuablePolicy'
|
||||||
|
@ -18,11 +21,12 @@ module Gitlab
|
||||||
# finder - The finder class to use for retrieving the issuables.
|
# finder - The finder class to use for retrieving the issuables.
|
||||||
# fast_fail - restrict counting to a shorter period, degrading gracefully on
|
# fast_fail - restrict counting to a shorter period, degrading gracefully on
|
||||||
# failure
|
# failure
|
||||||
def initialize(finder, project = nil, fast_fail: false)
|
def initialize(finder, project = nil, fast_fail: false, store_in_redis_cache: false)
|
||||||
@finder = finder
|
@finder = finder
|
||||||
@project = project
|
@project = project
|
||||||
@fast_fail = fast_fail
|
@fast_fail = fast_fail
|
||||||
@cache = Gitlab::SafeRequestStore[CACHE_KEY] ||= initialize_cache
|
@cache = Gitlab::SafeRequestStore[CACHE_KEY] ||= initialize_cache
|
||||||
|
@store_in_redis_cache = store_in_redis_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_state_or_opened(state = nil)
|
def for_state_or_opened(state = nil)
|
||||||
|
@ -52,7 +56,16 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def cache_for_finder
|
def cache_for_finder
|
||||||
@cache[@finder]
|
cached_counts = Rails.cache.read(redis_cache_key, cache_options) if cache_issues_count?
|
||||||
|
|
||||||
|
cached_counts ||= @cache[finder]
|
||||||
|
return cached_counts if cached_counts.empty?
|
||||||
|
|
||||||
|
if cache_issues_count? && cached_counts.values.all? { |count| count >= THRESHOLD }
|
||||||
|
Rails.cache.write(redis_cache_key, cached_counts, cache_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
cached_counts
|
||||||
end
|
end
|
||||||
|
|
||||||
def cast_state_to_symbol?(state)
|
def cast_state_to_symbol?(state)
|
||||||
|
@ -108,5 +121,33 @@ module Gitlab
|
||||||
"Count of failed calls to IssuableFinder#count_by_state with fast failure"
|
"Count of failed calls to IssuableFinder#count_by_state with fast failure"
|
||||||
).increment
|
).increment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cache_issues_count?
|
||||||
|
@store_in_redis_cache &&
|
||||||
|
finder.instance_of?(IssuesFinder) &&
|
||||||
|
parent_group.present? &&
|
||||||
|
!params_include_filters?
|
||||||
|
end
|
||||||
|
|
||||||
|
def parent_group
|
||||||
|
finder.params.group
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_cache_key
|
||||||
|
['group', parent_group&.id, 'issues']
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_options
|
||||||
|
{ expires_in: CACHE_EXPIRES_IN }
|
||||||
|
end
|
||||||
|
|
||||||
|
def params_include_filters?
|
||||||
|
non_filtering_params = %i[
|
||||||
|
scope state sort group_id include_subgroups
|
||||||
|
attempt_group_search_optimizations non_archived issue_types
|
||||||
|
]
|
||||||
|
|
||||||
|
finder.params.except(*non_filtering_params).values.any?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,8 +21,8 @@ module Sidebars
|
||||||
{}
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
def format_cached_count(count_service, count)
|
def format_cached_count(threshold, count)
|
||||||
if count > count_service::CACHED_COUNT_THRESHOLD
|
if count > threshold
|
||||||
number_to_human(
|
number_to_human(
|
||||||
count,
|
count,
|
||||||
units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
|
units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
|
||||||
|
|
|
@ -38,7 +38,7 @@ module Sidebars
|
||||||
count_service = ::Groups::OpenIssuesCountService
|
count_service = ::Groups::OpenIssuesCountService
|
||||||
count = count_service.new(context.group, context.current_user).count
|
count = count_service.new(context.group, context.current_user).count
|
||||||
|
|
||||||
format_cached_count(count_service, count)
|
format_cached_count(count_service::CACHED_COUNT_THRESHOLD, count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ module Sidebars
|
||||||
count_service = ::Groups::MergeRequestsCountService
|
count_service = ::Groups::MergeRequestsCountService
|
||||||
count = count_service.new(context.group, context.current_user).count
|
count = count_service.new(context.group, context.current_user).count
|
||||||
|
|
||||||
format_cached_count(count_service, count)
|
format_cached_count(count_service::CACHED_COUNT_THRESHOLD, count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,9 @@ function bundle_install_script() {
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
bundle --version
|
bundle --version
|
||||||
bundle config set path 'vendor'
|
bundle config set path "$(pwd)/vendor"
|
||||||
bundle config set clean 'true'
|
bundle config set clean 'true'
|
||||||
|
test -d jh && bundle config set gemfile 'jh/Gemfile'
|
||||||
|
|
||||||
echo "${BUNDLE_WITHOUT}"
|
echo "${BUNDLE_WITHOUT}"
|
||||||
bundle config
|
bundle config
|
||||||
|
|
|
@ -94,6 +94,41 @@ RSpec.describe 'Group issues page' do
|
||||||
expect(page).not_to have_content issue.title[0..80]
|
expect(page).not_to have_content issue.title[0..80]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when cached issues state count is enabled', :clean_gitlab_redis_cache do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(cached_issues_state_count: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'truncates issue counts if over the threshold' do
|
||||||
|
allow(Rails.cache).to receive(:read).and_call_original
|
||||||
|
allow(Rails.cache).to receive(:read).with(
|
||||||
|
['group', group.id, 'issues'],
|
||||||
|
{ expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
|
||||||
|
).and_return({ opened: 1050, closed: 500, all: 1550 })
|
||||||
|
|
||||||
|
visit issues_group_path(group)
|
||||||
|
|
||||||
|
expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when cached issues state count is disabled', :clean_gitlab_redis_cache do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(cached_issues_state_count: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not truncate counts if they are over the threshold' do
|
||||||
|
allow_next_instance_of(IssuesFinder) do |finder|
|
||||||
|
allow(finder).to receive(:count_by_state).and_return(true)
|
||||||
|
.and_return({ opened: 1050, closed: 500, all: 1550 })
|
||||||
|
end
|
||||||
|
|
||||||
|
visit issues_group_path(group)
|
||||||
|
|
||||||
|
expect(page).to have_text('Open 1,050 Closed 500 All 1,550')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'manual ordering', :js do
|
context 'manual ordering', :js do
|
||||||
|
|
|
@ -13,7 +13,11 @@ describe('Design management design scaler component', () => {
|
||||||
const setScale = (scale) => wrapper.vm.setScale(scale);
|
const setScale = (scale) => wrapper.vm.setScale(scale);
|
||||||
|
|
||||||
const createComponent = () => {
|
const createComponent = () => {
|
||||||
wrapper = shallowMount(DesignScaler);
|
wrapper = shallowMount(DesignScaler, {
|
||||||
|
propsData: {
|
||||||
|
maxScale: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -61,6 +65,18 @@ describe('Design management design scaler component', () => {
|
||||||
expect(wrapper.emitted('scale')).toEqual([[1.2]]);
|
expect(wrapper.emitted('scale')).toEqual([[1.2]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('computes & increments correct stepSize based on maxScale', async () => {
|
||||||
|
wrapper.setProps({ maxScale: 11 });
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
getIncreaseScaleButton().vm.$emit('click');
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.emitted().scale[0][0]).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
describe('when `scale` value is 1', () => {
|
describe('when `scale` value is 1', () => {
|
||||||
it('disables the "reset" button', () => {
|
it('disables the "reset" button', () => {
|
||||||
const resetButton = getResetScaleButton();
|
const resetButton = getResetScaleButton();
|
||||||
|
@ -77,7 +93,7 @@ describe('Design management design scaler component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when `scale` value is 2 (maximum)', () => {
|
describe('when `scale` value is maximum', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
setScale(2);
|
setScale(2);
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
|
@ -25,7 +25,9 @@ exports[`Design management design index page renders design index 1`] = `
|
||||||
<div
|
<div
|
||||||
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
||||||
>
|
>
|
||||||
<design-scaler-stub />
|
<design-scaler-stub
|
||||||
|
maxscale="2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -186,7 +188,9 @@ exports[`Design management design index page with error GlAlert is rendered in c
|
||||||
<div
|
<div
|
||||||
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
|
||||||
>
|
>
|
||||||
<design-scaler-stub />
|
<design-scaler-stub
|
||||||
|
maxscale="2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,17 @@ import { cloneDeep } from 'lodash';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||||
import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
|
import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import {
|
import {
|
||||||
|
getIssuesCountsQueryResponse,
|
||||||
getIssuesQueryResponse,
|
getIssuesQueryResponse,
|
||||||
filteredTokens,
|
filteredTokens,
|
||||||
locationSearch,
|
locationSearch,
|
||||||
urlParams,
|
urlParams,
|
||||||
getIssuesCountQueryResponse,
|
|
||||||
} from 'jest/issues_list/mock_data';
|
} from 'jest/issues_list/mock_data';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
@ -97,12 +97,12 @@ describe('IssuesListApp component', () => {
|
||||||
const mountComponent = ({
|
const mountComponent = ({
|
||||||
provide = {},
|
provide = {},
|
||||||
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
|
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
|
||||||
issuesQueryCountResponse = jest.fn().mockResolvedValue(getIssuesCountQueryResponse),
|
issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
|
||||||
mountFn = shallowMount,
|
mountFn = shallowMount,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const requestHandlers = [
|
const requestHandlers = [
|
||||||
[getIssuesQuery, issuesQueryResponse],
|
[getIssuesQuery, issuesQueryResponse],
|
||||||
[getIssuesCountQuery, issuesQueryCountResponse],
|
[getIssuesCountsQuery, issuesCountsQueryResponse],
|
||||||
];
|
];
|
||||||
const apolloProvider = createMockApollo(requestHandlers);
|
const apolloProvider = createMockApollo(requestHandlers);
|
||||||
|
|
||||||
|
@ -571,9 +571,9 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
describe('errors', () => {
|
describe('errors', () => {
|
||||||
describe.each`
|
describe.each`
|
||||||
error | mountOption | message
|
error | mountOption | message
|
||||||
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
|
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
|
||||||
${'fetching issue counts'} | ${'issuesQueryCountResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
|
${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
|
||||||
`('when there is an error $error', ({ mountOption, message }) => {
|
`('when there is an error $error', ({ mountOption, message }) => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = mountComponent({
|
wrapper = mountComponent({
|
||||||
|
@ -696,7 +696,11 @@ describe('IssuesListApp component', () => {
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalledWith({ message: IssuesListApp.i18n.reorderError });
|
expect(createFlash).toHaveBeenCalledWith({
|
||||||
|
message: IssuesListApp.i18n.reorderError,
|
||||||
|
captureError: true,
|
||||||
|
error: new Error('Request failed with status code 500'),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -70,10 +70,16 @@ export const getIssuesQueryResponse = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIssuesCountQueryResponse = {
|
export const getIssuesCountsQueryResponse = {
|
||||||
data: {
|
data: {
|
||||||
project: {
|
project: {
|
||||||
issues: {
|
openedIssues: {
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
closedIssues: {
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
allIssues: {
|
||||||
count: 1,
|
count: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -123,7 +123,7 @@ RSpec.describe IssuablesHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#issuables_state_counter_text' do
|
describe '#issuables_state_counter_text' do
|
||||||
let(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
describe 'state text' do
|
describe 'state text' do
|
||||||
context 'when number of issuables can be generated' do
|
context 'when number of issuables can be generated' do
|
||||||
|
@ -159,6 +159,38 @@ RSpec.describe IssuablesHelper do
|
||||||
.to eq('<span>All</span>')
|
.to eq('<span>All</span>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when count is over the threshold' do
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:issuables_count_for_state).and_return(1100)
|
||||||
|
allow(helper).to receive(:parent).and_return(group)
|
||||||
|
stub_const("Gitlab::IssuablesCountForState::THRESHOLD", 1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag cached_issues_state_count is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(cached_issues_state_count: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns complete count' do
|
||||||
|
expect(helper.issuables_state_counter_text(:issues, :opened, true))
|
||||||
|
.to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm">1,100</span>')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag cached_issues_state_count is enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(cached_issues_state_count: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns truncated count' do
|
||||||
|
expect(helper.issuables_state_counter_text(:issues, :opened, true))
|
||||||
|
.to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm">1.1k</span>')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
|
||||||
|
|
||||||
subject(:migrate_pages_metadata) { described_class.new }
|
subject(:migrate_pages_metadata) { described_class.new }
|
||||||
|
|
||||||
describe '#perform_on_relation' do
|
describe '#perform' do
|
||||||
let(:namespaces) { table(:namespaces) }
|
let(:namespaces) { table(:namespaces) }
|
||||||
let(:builds) { table(:ci_builds) }
|
let(:builds) { table(:ci_builds) }
|
||||||
let(:pages_metadata) { table(:project_pages_metadata) }
|
let(:pages_metadata) { table(:project_pages_metadata) }
|
||||||
|
@ -23,9 +23,9 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
|
||||||
not_migrated_no_pages = projects.create!(namespace_id: namespace.id, name: 'Not Migrated No Pages')
|
not_migrated_no_pages = projects.create!(namespace_id: namespace.id, name: 'Not Migrated No Pages')
|
||||||
project_not_in_relation_scope = projects.create!(namespace_id: namespace.id, name: 'Other')
|
project_not_in_relation_scope = projects.create!(namespace_id: namespace.id, name: 'Other')
|
||||||
|
|
||||||
projects_relation = projects.where(id: [not_migrated_with_pages, not_migrated_no_pages, migrated])
|
ids = [not_migrated_no_pages.id, not_migrated_with_pages.id, migrated.id]
|
||||||
|
|
||||||
migrate_pages_metadata.perform_on_relation(projects_relation)
|
migrate_pages_metadata.perform(ids.min, ids.max)
|
||||||
|
|
||||||
expect(pages_metadata.find_by_project_id(not_migrated_with_pages.id).deployed).to eq(true)
|
expect(pages_metadata.find_by_project_id(not_migrated_with_pages.id).deployed).to eq(true)
|
||||||
expect(pages_metadata.find_by_project_id(not_migrated_no_pages.id).deployed).to eq(false)
|
expect(pages_metadata.find_by_project_id(not_migrated_no_pages.id).deployed).to eq(false)
|
||||||
|
@ -33,12 +33,4 @@ RSpec.describe Gitlab::BackgroundMigration::MigratePagesMetadata, schema: 201909
|
||||||
expect(pages_metadata.find_by_project_id(project_not_in_relation_scope.id)).to be_nil
|
expect(pages_metadata.find_by_project_id(project_not_in_relation_scope.id)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
|
||||||
it 'creates relation and delegates to #perform_on_relation' do
|
|
||||||
expect(migrate_pages_metadata).to receive(:perform_on_relation).with(projects.where(id: 3..5))
|
|
||||||
|
|
||||||
migrate_pages_metadata.perform(3, 5)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,4 +66,106 @@ RSpec.describe Gitlab::IssuablesCountForState do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when store_in_redis_cache is `true`', :clean_gitlab_redis_cache do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
|
let(:cache_options) { { expires_in: 10.minutes } }
|
||||||
|
let(:cache_key) { ['group', group.id, 'issues'] }
|
||||||
|
let(:threshold) { described_class::THRESHOLD }
|
||||||
|
let(:states_count) { { opened: 1, closed: 1, all: 2 } }
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
subject { described_class.new(finder, fast_fail: true, store_in_redis_cache: true ) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(finder).to receive(:count_by_state).and_return(states_count)
|
||||||
|
allow_next_instance_of(described_class) do |counter|
|
||||||
|
allow(counter).to receive(:parent_group).and_return(group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'calculating counts without caching' do
|
||||||
|
it 'does not store in redis store' do
|
||||||
|
expect(Rails.cache).not_to receive(:read)
|
||||||
|
expect(finder).to receive(:count_by_state)
|
||||||
|
expect(Rails.cache).not_to receive(:write)
|
||||||
|
expect(subject[:all]).to eq(states_count[:all])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Issues' do
|
||||||
|
let(:finder) { IssuesFinder.new(user, params) }
|
||||||
|
|
||||||
|
it 'returns -1 for the requested state' do
|
||||||
|
allow(finder).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
|
||||||
|
expect(Rails.cache).not_to receive(:write)
|
||||||
|
|
||||||
|
expect(subject[:all]).to eq(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when parent group is not present' do
|
||||||
|
let(:group) { nil }
|
||||||
|
|
||||||
|
it_behaves_like 'calculating counts without caching'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when params include search filters' do
|
||||||
|
let(:parent) { group }
|
||||||
|
|
||||||
|
before do
|
||||||
|
finder.params[:assignee_username] = [user.username, 'root']
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'calculating counts without caching'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when counts are stored in cache' do
|
||||||
|
before do
|
||||||
|
allow(Rails.cache).to receive(:read).with(cache_key, cache_options)
|
||||||
|
.and_return({ opened: 1000, closed: 1000, all: 2000 })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call finder count_by_state' do
|
||||||
|
expect(finder).not_to receive(:count_by_state)
|
||||||
|
|
||||||
|
expect(subject[:all]).to eq(2000)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when cache is empty' do
|
||||||
|
context 'when state counts are under threshold' do
|
||||||
|
let(:states_count) { { opened: 1, closed: 1, all: 2 } }
|
||||||
|
|
||||||
|
it 'does not store state counts in cache' do
|
||||||
|
expect(Rails.cache).to receive(:read).with(cache_key, cache_options)
|
||||||
|
expect(finder).to receive(:count_by_state)
|
||||||
|
expect(Rails.cache).not_to receive(:write)
|
||||||
|
expect(subject[:all]).to eq(states_count[:all])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when state counts are over threshold' do
|
||||||
|
let(:states_count) do
|
||||||
|
{ opened: threshold + 1, closed: threshold + 1, all: (threshold + 1) * 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores state counts in cache' do
|
||||||
|
expect(Rails.cache).to receive(:read).with(cache_key, cache_options)
|
||||||
|
expect(finder).to receive(:count_by_state)
|
||||||
|
expect(Rails.cache).to receive(:write).with(cache_key, states_count, cache_options)
|
||||||
|
|
||||||
|
expect(subject[:all]).to eq((threshold + 1) * 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Merge Requests' do
|
||||||
|
let(:finder) { MergeRequestsFinder.new(user, params) }
|
||||||
|
|
||||||
|
it_behaves_like 'calculating counts without caching'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5999,4 +5999,49 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#groups_with_developer_maintainer_project_access' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:group1) { create(:group) }
|
||||||
|
|
||||||
|
let_it_be(:developer_group1) do
|
||||||
|
create(:group).tap do |g|
|
||||||
|
g.add_developer(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:developer_group2) do
|
||||||
|
create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
|
||||||
|
g.add_developer(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:guest_group1) do
|
||||||
|
create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
|
||||||
|
g.add_guest(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:developer_group1) do
|
||||||
|
create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS).tap do |g|
|
||||||
|
g.add_maintainer(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { user.send(:groups_with_developer_maintainer_project_access) }
|
||||||
|
|
||||||
|
shared_examples 'groups_with_developer_maintainer_project_access examples' do
|
||||||
|
specify { is_expected.to contain_exactly(developer_group2) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'groups_with_developer_maintainer_project_access examples'
|
||||||
|
|
||||||
|
context 'when feature flag :linear_user_groups_with_developer_maintainer_project_access is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(linear_user_groups_with_developer_maintainer_project_access: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'groups_with_developer_maintainer_project_access examples'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue