diff options
Diffstat (limited to 'app')
51 files changed, 541 insertions, 129 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue new file mode 100644 index 00000000000..708121ee210 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue @@ -0,0 +1,127 @@ +<script> +import uniqueId from 'lodash/uniqueId'; +import { GlIcon, GlTooltip, GlDisclosureDropdown } from '@gitlab/ui'; +import DisclosureHierarchyItem from './disclosure_hierarchy_item.vue'; + +export default { + components: { + GlDisclosureDropdown, + GlIcon, + GlTooltip, + DisclosureHierarchyItem, + }, + props: { + /** + * A list of items in the form: + * ``` + * { + * title: String, required + * icon: String, optional + * } + * ``` + */ + items: { + type: Array, + required: false, + default: () => [], + validator: (items) => { + return items.every((item) => Object.keys(item).includes('title')); + }, + }, + /** + * When set, displays only first and last item, and groups the rest under an ellipsis button + */ + withEllipsis: { + type: Boolean, + default: false, + required: false, + }, + /** + * When set, a tooltip displays when hovering middle ellipsis button + */ + ellipsisTooltipLabel: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + itemUuid: uniqueId('disclosure-hierarchy-'), + }; + }, + computed: { + middleItems() { + return this.items.slice(1, -1).map((item) => ({ ...item, text: item.title })); + }, + firstItem() { + return this.items[0]; + }, + lastItemIndex() { + return this.items.length - 1; + }, + lastItem() { + return this.items[this.lastItemIndex]; + }, + }, + methods: { + itemId(index) { + return `${this.itemUuid}-item-${index}`; + }, + }, +}; +</script> + +<template> + <div class="gl-relative gl-display-flex"> + <ul class="gl-p-0 gl-m-0 gl-relative gl-list-style-none gl-display-inline-flex gl-w-85p"> + <template v-if="withEllipsis"> + <disclosure-hierarchy-item :item="firstItem" :item-id="itemId(0)"> + <slot :item="firstItem" :item-id="itemId(0)"></slot> + </disclosure-hierarchy-item> + <li class="disclosure-hierarchy-item"> + <gl-disclosure-dropdown :items="middleItems"> + <template #toggle> + <button + id="disclosure-hierarchy-ellipsis-button" + class="disclosure-hierarchy-button" + :aria-label="ellipsisTooltipLabel" + > + <gl-icon name="ellipsis_h" class="gl-ml-3 gl-text-gray-600 gl-z-index-200" /> + </button> + </template> + <template #list-item="{ item }"> + <span class="gl-display-flex"> + <gl-icon + v-if="item.icon" + :name="item.icon" + class="gl-mr-3 gl-vertical-align-middle gl-text-gray-600 gl-flex-shrink-0" + /> + {{ item.title }} + </span> + </template> + </gl-disclosure-dropdown> + </li> + <gl-tooltip + v-if="ellipsisTooltipLabel" + target="disclosure-hierarchy-ellipsis-button" + triggers="hover" + > + {{ ellipsisTooltipLabel }} + </gl-tooltip> + <disclosure-hierarchy-item :item="lastItem" :item-id="itemId(lastItemIndex)"> + <slot :item="lastItem" :item-id="itemId(lastItemIndex)"></slot> + </disclosure-hierarchy-item> + </template> + <disclosure-hierarchy-item + v-for="(item, index) in items" + v-else + :key="index" + :item="item" + :item-id="itemId(index)" + > + <slot :item="item" :item-id="itemId(index)"></slot> + </disclosure-hierarchy-item> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue new file mode 100644 index 00000000000..8347583f3c5 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue @@ -0,0 +1,61 @@ +<!-- eslint-disable vue/multi-word-component-names --> +<script> +import iconSpriteInfo from '@gitlab/svgs/dist/icons.json'; +import { GlIcon, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlLink, + }, + props: { + /** + * Path item in the form: + * ``` + * { + * title: String, required + * icon: String, optional + * } + * ``` + */ + item: { + type: Object, + required: false, + default: () => {}, + }, + itemId: { + type: String, + required: false, + default: null, + }, + }, + methods: { + shouldDisplayIcon(icon) { + return icon && iconSpriteInfo.icons.includes(icon); + }, + }, +}; +</script> + +<template> + <li class="disclosure-hierarchy-item"> + <gl-link + :id="itemId" + :href="item.webUrl" + class="disclosure-hierarchy-button gl-text-gray-900 gl-hover-text-decoration-none gl-active-text-decoration-none!" + > + <gl-icon + v-if="shouldDisplayIcon(item.icon)" + :name="item.icon" + class="gl-mx-2 gl-text-gray-600 gl-flex-shrink-0" + /> + <span class="gl-z-index-200 gl-text-truncate">{{ item.title }}</span> + </gl-link> + <!-- + @slot Additional content to be displayed in an item. + @binding {Object} item The item being rendered. + @binding {String} itemId The rendered item's ID. + --> + <slot :item="item" :item-id="itemId"></slot> + </li> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue new file mode 100644 index 00000000000..bebe5d64761 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue @@ -0,0 +1,95 @@ +<script> +import { GlIcon, GlPopover, GlBadge, GlSprintf } from '@gitlab/ui'; + +import { createAlert } from '~/alert'; +import { s__ } from '~/locale'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { formatAncestors } from '../../utils'; +import workItemAncestorsQuery from '../../graphql/work_item_ancestors.query.graphql'; +import WorkItemStateBadge from '../work_item_state_badge.vue'; +import DisclosureHierarchy from './disclosure_hierarchy.vue'; + +export default { + i18n: { + ancestorLabel: s__('WorkItem|Ancestor'), + ancestorsTooltipLabel: s__('WorkItem|Show all ancestors'), + }, + components: { + GlIcon, + GlPopover, + GlBadge, + GlSprintf, + TimeAgoTooltip, + WorkItemStateBadge, + DisclosureHierarchy, + }, + props: { + workItem: { + type: Object, + required: true, + }, + }, + data() { + return { + ancestors: [], + }; + }, + apollo: { + ancestors: { + query: workItemAncestorsQuery, + variables() { + return { + id: this.workItem.id, + }; + }, + update(data) { + return formatAncestors(data.workItem); + }, + skip() { + return !this.workItem.id; + }, + error(error) { + createAlert({ + message: s__('Hierarchy|Something went wrong while fetching ancestors.'), + captureError: true, + error, + }); + }, + }, + }, +}; +</script> + +<template> + <disclosure-hierarchy + v-if="ancestors.length > 0" + class="gl-mr-auto" + :items="ancestors" + :with-ellipsis="ancestors.length > 2" + :ellipsis-tooltip-label="$options.i18n.ancestorsTooltipLabel" + > + <template #default="{ item, itemId }"> + <gl-popover triggers="hover focus" placement="bottom" :target="itemId"> + <template #title> + <gl-badge variant="muted" size="sm">{{ $options.i18n.ancestorLabel }}</gl-badge> + <div class="gl-pt-3"> + {{ item.title }} + </div> + </template> + <div class="gl-pb-3 gl-text-gray-500"> + <gl-icon v-if="item.icon" :name="item.icon" /> + <span>{{ item.reference }}</span> + </div> + <work-item-state-badge v-if="item.state" :work-item-state="item.state" /> + <span class="gl-text-gray-500"> + <gl-sprintf v-if="item.createdAt" :message="__('Created %{timeAgo}')"> + <template #timeAgo> + <time-ago-tooltip :time="item.createdAt" /> + </template> + </gl-sprintf> + </span> + </gl-popover> + </template> + </disclosure-hierarchy> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 2440fc7d433..5d8c41176d0 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -1,13 +1,6 @@ <script> import { isEmpty } from 'lodash'; -import { - GlAlert, - GlSkeletonLoader, - GlIcon, - GlButton, - GlTooltipDirective, - GlEmptyState, -} from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui'; import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw'; import { s__ } from '~/locale'; import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility'; @@ -48,8 +41,8 @@ import WorkItemNotes from './work_item_notes.vue'; import WorkItemDetailModal from './work_item_detail_modal.vue'; import WorkItemAwardEmoji from './work_item_award_emoji.vue'; import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue'; -import WorkItemTypeIcon from './work_item_type_icon.vue'; import WorkItemStickyHeader from './work_item_sticky_header.vue'; +import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue'; export default { i18n, @@ -61,7 +54,6 @@ export default { GlAlert, GlButton, GlSkeletonLoader, - GlIcon, GlEmptyState, WorkItemActions, WorkItemTodos, @@ -70,13 +62,13 @@ export default { WorkItemAwardEmoji, WorkItemTitle, WorkItemAttributesWrapper, - WorkItemTypeIcon, WorkItemTree, WorkItemNotes, WorkItemDetailModal, AbuseCategorySelector, WorkItemRelationships, WorkItemStickyHeader, + WorkItemAncestors, }, mixins: [glFeatureFlagMixin()], inject: ['fullPath', 'isGroup', 'reportAbusePath'], @@ -427,37 +419,9 @@ export default { /> </div> <div - class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3" + class="gl-display-block gl-md-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row flex-wrap gl-gap-3 gl-pt-3" > - <ul - v-if="parentWorkItem" - class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0" - data-testid="work-item-parent" - > - <li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-min-w-0"> - <gl-button - v-gl-tooltip.hover - class="gl-text-truncate" - :icon="parentWorkItemIconName" - category="tertiary" - :href="parentUrl" - :title="parentWorkItemReference" - @click="openInModal({ event: $event, modalWorkItem: parentWorkItem })" - >{{ parentWorkItemReference }}</gl-button - > - <gl-icon name="chevron-right" :size="16" class="gl-flex-shrink-0" /> - </li> - <li - class="gl-px-4 gl-py-3 gl-line-height-0 gl-display-flex gl-align-items-center gl-overflow-hidden gl-flex-shrink-0" - > - <work-item-type-icon - :work-item-icon-name="workItemIconName" - :work-item-type="workItemType" - show-text - /> - {{ workItemBreadcrumbReference }} - </li> - </ul> + <work-item-ancestors v-if="parentWorkItem" :work-item="workItem" class="gl-mb-1" /> <div v-if="!error && !workItemLoading" :class="titleClassHeader" @@ -475,7 +439,9 @@ export default { @error="updateError = $event" /> </div> - <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3"> + <div + class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3" + > <work-item-todos v-if="showWorkItemCurrentUserTodos" :work-item-id="workItem.id" diff --git a/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql new file mode 100644 index 00000000000..bfcac11f51f --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql @@ -0,0 +1,33 @@ +query workItemAncestorsQuery($id: WorkItemID!) { + workItem(id: $id) { + id + title + widgets { + type + ... on WorkItemWidgetHierarchy { + type + parent { + id + } + ancestors { + nodes { + id + iid + confidential + workItemType { + id + name + iconName + } + title + state + reference(full: true) + createdAt + closedAt + webUrl + } + } + } + } + } +} diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js index ac5d8b32fad..221131f6854 100644 --- a/app/assets/javascripts/work_items/utils.js +++ b/app/assets/javascripts/work_items/utils.js @@ -26,6 +26,16 @@ export const findHierarchyWidgets = (widgets) => export const findHierarchyWidgetChildren = (workItem) => findHierarchyWidgets(workItem?.widgets)?.children?.nodes || []; +export const findHierarchyWidgetAncestors = (workItem) => + findHierarchyWidgets(workItem?.widgets)?.ancestors?.nodes || []; + +export const formatAncestors = (workItem) => + findHierarchyWidgetAncestors(workItem).map((ancestor) => ({ + ...ancestor, + icon: ancestor.workItemType?.iconName, + href: ancestor.webUrl, + })); + const autocompleteSourcesPath = (autocompleteType, fullPath, workItemIid) => { return `${ gon.relative_url_root || '' diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index 2030f2c7095..97f2add4e77 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -335,7 +335,7 @@ border-radius: 3px; margin-left: 4px; margin-top: -2px; - border: 1px solid $black-transparent; + border: 1px solid $t-gray-a-24; background-color: var(--gl-color-chip-color); } diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss index a5fd57f6c57..56214040fdd 100644 --- a/app/assets/stylesheets/components/detail_page.scss +++ b/app/assets/stylesheets/components/detail_page.scss @@ -38,18 +38,9 @@ .detail-page-header-actions { flex: 0 0 auto; - &:not(.is-merge-request) { - @include media-breakpoint-down(xs) { - width: 100%; - margin-top: 10px; - } - } - - &.is-merge-request { - @include media-breakpoint-down(sm) { - width: 100%; - margin-top: 10px; - } + @include media-breakpoint-down(sm) { + width: 100%; + margin-top: 10px; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 88509dbc4a1..07539b59574 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -118,7 +118,7 @@ } @mixin btn-white { - @include btn-color($white, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color); + @include btn-color($white, $border-color, $gray-50, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color); } @mixin btn-purple { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index aa5bd4cf098..e7d158de7e7 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -495,7 +495,7 @@ li.note { width: 4px; &:hover { - background-color: $white-normal; + background-color: $gray-50; } &.is-dragging { diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index fb9816d1402..defe67b68ad 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -446,7 +446,7 @@ &.mobile-nav-open { display: block; position: fixed; - background-color: $black-transparent; + background-color: $t-gray-a-24; height: 100%; width: 100%; z-index: $zindex-dropdown-menu; diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index fff42c0973c..b948a57ea33 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -689,7 +689,7 @@ table.code { .note-container { background-color: $gray-light; - border-top: 1px solid $white-normal; + border-top: 1px solid $gray-50; // double jagged line divider .discussion-notes + .discussion-notes::before, @@ -750,7 +750,7 @@ table.code { .diff-file .note-container > .new-note, .note-container .discussion-notes.diff-discussions { margin-left: 100px; - border-left: 1px solid $white-normal; + border-left: 1px solid $gray-50; } .notes.active { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a467d9e8c8a..b21504a6e31 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -846,7 +846,7 @@ } .loading-animation { - color: $almost-black; + color: $gray-950; } .frequent-items-dropdown-content { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index c99e985e18c..9cb264c992b 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -376,7 +376,7 @@ span.idiff { border-bottom: 1px $gray-darkest dashed; &:hover { - border-bottom-color: $almost-black; + border-bottom-color: $gray-950; } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 67e96f08cb0..5949a1b2809 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -110,7 +110,7 @@ } .operator { - background-color: $white-normal; + background-color: $gray-50; color: $gl-text-color; margin-right: 1px; } @@ -118,7 +118,7 @@ .value-container { display: flex; align-items: center; - background-color: $white-normal; + background-color: $gray-50; color: $gl-text-color; border-radius: 0 2px 2px 0; margin-right: 5px; diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 66d163f608a..b87a7f15c1c 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -45,6 +45,6 @@ height: 100%; margin-bottom: 2px; border-radius: 3px; - border: 1px solid $black-transparent; + border: 1px solid $t-gray-a-24; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index e9a507ebb6b..832b2297673 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -53,7 +53,7 @@ p { padding-top: 1px; margin: 0; - color: $white-normal; + color: $gray-50; img { position: relative; @@ -104,7 +104,7 @@ ul.content-list { padding: 0; li { - border-color: $white-normal; + border-color: $gray-50; font-size: $gl-font-size; color: $gl-text-color; word-break: break-word; @@ -165,7 +165,7 @@ ul.content-list { &.list-placeholder { background-color: $gray-light; - border: dotted 1px $white-normal; + border: dotted 1px $gray-50; margin: 1px 0; min-height: 52px; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index f76a9cf0373..0265820bfe1 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -85,7 +85,7 @@ body.modal-open { } .modal { - background-color: $black-transparent; + background-color: $t-gray-a-24; .modal-content { border-radius: $modal-border-radius; diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index f57d906e73c..5a86a96a96e 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -38,7 +38,7 @@ border: 0; &:not(:last-child) { - border-bottom: 1px solid $white-normal; + border-bottom: 1px solid $gray-50; } } } @@ -69,7 +69,7 @@ min-height: 62px; &:not(:first-child) { - border-top: 1px solid $white-normal; + border-top: 1px solid $gray-50; } } diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index c8bf2877b5a..93b7768eb99 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -245,7 +245,7 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4; bottom: 0; left: 0; right: 0; - background-color: $black-transparent; + background-color: $t-gray-a-24; z-index: $super-sidebar-z-index - 1; @include media-breakpoint-up(md) { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1964487c5e0..18c60009537 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -108,17 +108,13 @@ $t-gray-a-08: rgba($gray-950, 0.08) !default; $t-gray-a-16: rgba($gray-950, 0.16) !default; $t-gray-a-24: rgba($gray-950, 0.24) !default; -$white-normal: $gray-50 !default; $white-dark: darken($gray-50, 2) !default; -$black-transparent: $t-gray-a-24 !default; -$almost-black: $gray-950 !default; - // To do this variant right for darkmode, we need to create a variable for it. $indigo-900-alpha-008: rgba($theme-indigo-900, 0.08); $border-white-light: darken($white, $darken-border-factor) !default; -$border-white-normal: darken($white-normal, $darken-border-factor) !default; +$border-white-normal: darken($gray-50, $darken-border-factor) !default; $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); @@ -315,7 +311,7 @@ $dark-diff-match-color: rgba($white, 0.1); $diff-image-info-color: #808080; $diff-view-modes-color: #808080; $diff-view-modes-border: #c1c1c1; -$diff-jagged-border-gradient-color: darken($white-normal, 8%); +$diff-jagged-border-gradient-color: darken($gray-50, 8%); /* * Fonts diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index 085e25a0cdc..23fa1326881 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -132,7 +132,7 @@ } } -@mixin line-hover-bg($color: $white-normal) { +@mixin line-hover-bg($color: $gray-50) { &:hover, &:focus-within { background-color: darken($color, 10); diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index f36eaa663e5..c2bc35ec91a 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -5,8 +5,8 @@ @import '../common'; @mixin match-line { - color: $black-transparent; - background-color: $white-normal; + color: $t-gray-a-24; + background-color: $gray-50; } :root { @@ -40,13 +40,13 @@ .diff-line-num, .diff-line-num a { - color: $black-transparent; + color: $t-gray-a-24; } // Code itself pre.code, .diff-line-num { - border-color: $white-normal; + border-color: $gray-50; } &, @@ -86,7 +86,7 @@ &.new, &.new-nomappinginraw, &.old-nomappinginraw { - background-color: $white-normal; + background-color: $gray-50; } } @@ -137,27 +137,27 @@ .line_content { &.old, &.old-nomappinginraw { - background-color: $white-normal; + background-color: $gray-50; &::before { color: $gl-text-color; } span.idiff { - background-color: $white-normal; + background-color: $gray-50; text-decoration: underline; } } &.new:not(.hll), &.new-nomappinginraw:not(.hll) { - background-color: $white-normal; + background-color: $gray-50; &::before { color: $gl-text-color; } span.idiff { - background-color: $white-normal; + background-color: $gray-50; text-decoration: underline; } } @@ -170,7 +170,7 @@ // Search result highlight span.highlight_word { - background-color: $white-normal; + background-color: $gray-50; } // Links to URLs, emails, or dependencies diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index b3aa10c3ace..c902d9357e8 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -101,7 +101,7 @@ $solarized-light-il: #2aa198; } @mixin match-line { - color: $black-transparent; + color: $t-gray-a-24; background: $solarized-light-matchline-bg; } diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 2631055706f..89d6d93614f 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -73,7 +73,7 @@ $white-gc-bg: #eaf2f5; @mixin match-line { - color: $black-transparent; + color: $t-gray-a-24; background-color: $gray-light; } diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index fd212d14e30..db8c3d163c0 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -93,8 +93,8 @@ $highlighted-gc-bg: #eaf2f5; text-align: right; width: 35px; background-color: $gray-light; - color: $black-transparent; - border-right: 1px solid $white-normal; + color: $t-gray-a-24; + border-right: 1px solid $gray-50; &.old { background-color: $line-number-old; @@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5; } &.match { - color: $black-transparent; + color: $t-gray-a-24; background-color: $gray-light; } } diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss index d93b4f75d77..0fb1b3c9c92 100644 --- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss @@ -92,7 +92,7 @@ } .line-numbers { - color: $black-transparent; + color: $t-gray-a-24; } .view-overlays { diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index c584bbaac09..b6caa845cfa 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -67,7 +67,7 @@ } .drag-handle:hover { - background-color: var(--ide-dropdown-hover-background, $white-normal); + background-color: var(--ide-dropdown-hover-background, $gray-50); } .card-header { @@ -221,7 +221,7 @@ .filtered-search-token .value-container, .filtered-search-term .value-container { - background-color: var(--ide-dropdown-hover-background, $white-normal); + background-color: var(--ide-dropdown-hover-background, $gray-50); color: var(--ide-text-color, $gl-text-color); &:hover { @@ -292,7 +292,7 @@ &:hover, &:focus { border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important; - background-color: var(--ide-btn-default-background, $white-normal) !important; + background-color: var(--ide-btn-default-background, $gray-50) !important; } &:active, diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 7f8068e5d56..45270ab3d30 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -436,7 +436,7 @@ $ide-commit-header-height: 48px; } &.is-active { - background-color: var(--ide-background, $white-normal); + background-color: var(--ide-background, $gray-50); } svg { @@ -545,7 +545,7 @@ $ide-commit-header-height: 48px; width: $ide-commit-row-height; height: $ide-commit-row-height; color: inherit; - background-color: var(--ide-background, $white-normal); + background-color: var(--ide-background, $gray-50); } .ide-commit-options { @@ -740,7 +740,7 @@ $ide-commit-header-height: 48px; background-color: var(--ide-input-background, transparent); &:hover { - background-color: var(--ide-dropdown-btn-hover-background, $white-normal); + background-color: var(--ide-dropdown-btn-hover-background, $gray-50); } svg { @@ -1063,7 +1063,7 @@ $ide-commit-header-height: 48px; &:active, &:focus { - color: $white-normal; + color: $gray-50; background-color: var(--ide-link-color, $blue-500); outline: 0; } @@ -1077,7 +1077,7 @@ $ide-commit-header-height: 48px; } .dropdown.show .ide-entry-dropdown-toggle { - color: $white-normal; + color: $gray-50; background-color: var(--ide-link-color, $blue-500); } } diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss index 71a7b3a7af0..70aeedb10bf 100644 --- a/app/assets/stylesheets/page_bundles/merge_request.scss +++ b/app/assets/stylesheets/page_bundles/merge_request.scss @@ -304,7 +304,7 @@ $comparison-empty-state-height: 62px; .merge-request-details .file-finder-overlay.diff-file-finder { position: fixed; z-index: 99999; - background: $black-transparent; + background: $t-gray-a-24; } .mr-compare { diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index 6b68fa75001..9bab5d65b59 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -218,7 +218,7 @@ min-width: 195px; left: 100%; top: -10px; - box-shadow: 0 1px 5px $black-transparent; + box-shadow: 0 1px 5px $t-gray-a-24; } .codequality-report { diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss index bcc0ad112ac..2bd8984c2ea 100644 --- a/app/assets/stylesheets/page_bundles/pipelines.scss +++ b/app/assets/stylesheets/page_bundles/pipelines.scss @@ -24,7 +24,7 @@ .btn.btn-retry:hover, .btn.btn-retry:focus { border-color: $dropdown-toggle-active-border-color; - background-color: $white-normal; + background-color: $gray-50; } svg path { @@ -42,7 +42,7 @@ } .btn-group.open .btn-default { - background-color: $white-normal; + background-color: $gray-50; border-color: $border-white-normal; } diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index ec73f27ed09..b9ab2450ff9 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -215,3 +215,130 @@ $work-item-sticky-header-height: 52px; font-weight: normal; } } + +// Disclosure hierarchy component, used for Ancestors widget + +$disclosure-hierarchy-chevron-dimension: 1.2rem; + +@mixin hierarchy-active-item-color { + background-color: var(--gray-50, $gray-50); + + &::after { + background-color: var(--gray-50, $gray-50); + } +} + +@mixin hierarchy-path-chevron { + content: ''; + @include gl-absolute; + @include gl-reset-bg; + top: 0.39rem; + right: px-to-rem(-9px); + width: $disclosure-hierarchy-chevron-dimension; + height: $disclosure-hierarchy-chevron-dimension; + transform: rotate(45deg) skew(14deg, 14deg); +} + +.disclosure-hierarchy-button { + @include gl-pl-4; + @include gl-py-3; + @include gl-display-flex; + @include gl-relative; + @include gl-font-sm; + border: 1px solid var(--gray-100, $gray-100); + @include gl-border-r-none; + @include gl-border-l-none; + @include gl-line-height-normal; + padding-right: $grid-size; + max-width: $gl-spacing-scale-20; + background: var(--gray-10, $white); + + @include media-breakpoint-up(sm) { + max-width: $gl-spacing-scale-48; + } + + &::before, + &::after { + @include hierarchy-path-chevron; + border: 1px solid var(--gray-100, $gray-100); + border-color: inherit; + @include gl-border-b-transparent; + @include gl-border-l-transparent; + @include gl-reset-bg; + @include gl-rounded-top-left-small; + @include gl-rounded-bottom-right-small; + } + + &::before { + background: var(--gray-10, $white); + left: -10px; + z-index: 1; + } + + &::after { + z-index: 0; + } + + .disclosure-hierarchy-item:first-child & { + @include gl-pl-3; + border-left: 1px solid var(--gray-100, $gray-100); + @include gl-rounded-top-left-base; + @include gl-rounded-bottom-left-base; + + &::before { + @include gl-display-none; + } + + &:active, + &:focus, + &:focus:active { + // Custom focus + box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400, -1px 1px 0 1px $blue-400, -1px -1px 0 1px $blue-400 !important; + } + } + + .disclosure-hierarchy-item:last-child & { + @include gl-pr-4; + border-right: 1px solid var(--gray-100, $gray-100); + @include gl-rounded-top-right-base; + @include gl-rounded-bottom-right-base; + + &::after { + display: none; + } + } + + &[disabled] { + color: $gl-text-color-disabled; + @include gl-cursor-not-allowed; + } + + &:not([disabled]):hover { + @include gl-border-gray-400; + @include hierarchy-active-item-color; + color: var(--gray-900, $gray-900); + + &::after { + border-left: 1px solid var(--gray-50, $gray-50); + border-bottom: 1px solid var(--gray-50, $gray-50); + z-index: 3; + } + } + + &:active, + &:focus, + &:focus:active { + // Custom focus + box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400 !important; + outline: none; + border-top: 1px solid var(--gray-400, $gray-400); + border-bottom: 1px solid var(--gray-400, $gray-400); + @include hierarchy-active-item-color; + z-index: 2; + @include gl-rounded-small; + + &::before, &::after { + box-shadow: 2px -2px 0 1px $blue-400; + } + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 72ea586979f..8511bc22725 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -79,7 +79,7 @@ .commits-row { + .commits-row { - border-top: 1px solid $white-normal; + border-top: 1px solid $gray-50; } + .commits-empty { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 269b3078495..cfb964e6227 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -5,7 +5,7 @@ .event-item { font-size: $gl-font-size; padding: $gl-padding 0 $gl-padding $gl-spacing-scale-8; - border-bottom: 1px solid $white-normal; + border-bottom: 1px solid $gray-50; color: $gl-text-color-secondary; position: relative; line-height: $gl-line-height-20; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 2e1bb9b9eac..a8f557270ba 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -179,7 +179,7 @@ table.pipeline-project-metrics tr td { } &:first-child { - border-top: 1px solid $white-normal; + border-top: 1px solid $gray-50; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 38686d5e713..e5808c71a6d 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -56,7 +56,7 @@ &.is-dropzone-hover { border-color: $green-500; - box-shadow: 0 0 2px $black-transparent, + box-shadow: 0 0 2px $t-gray-a-24, 0 0 4px $green-500-focus; .comment-toolbar, diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss index e249ecbd10b..f5787799fce 100644 --- a/app/assets/stylesheets/snippets.scss +++ b/app/assets/stylesheets/snippets.scss @@ -82,12 +82,12 @@ font-size: $code-font-size; line-height: $code-line-height; white-space: nowrap; - color: $black-transparent; + color: $t-gray-a-24; min-width: 30px; } .diff-line-num:hover { - color: $almost-black; + color: $gray-950; cursor: pointer; } } @@ -158,7 +158,7 @@ border-right: 0; &:hover { - background-color: $white-normal; + background-color: $gray-50; border-color: $border-white-normal; text-decoration: none; } diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index 303ea9dd005..0fce87faa29 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -28,7 +28,6 @@ $t-gray-a-16: rgba($gray-10, 0.16); $t-gray-a-24: rgba($gray-10, 0.24); $black-normal: $gray-900; -$white-normal: $gray-50; $white-dark: $gray-100; $theme-indigo-50: #1a1a40; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 347b8e20ab4..79ea8d3cc70 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -37,7 +37,7 @@ .border-color-default { border-color: $border-color; } .border-radius-default { border-radius: $border-radius-default; } .border-radius-small { border-radius: $border-radius-small; } -.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; } +.box-shadow-default { box-shadow: 0 2px 4px 0 $t-gray-a-24; } // Override Bootstrap class with offset for system-header and // performance bar when present diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6d8b2711734..fb73cb55ec0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -709,7 +709,7 @@ module Ci end def artifacts_public? - return true if Feature.disabled?(:non_public_artifacts, type: :development) + return true if Feature.disabled?(:non_public_artifacts, project, type: :development) return true if job_artifacts_archive.nil? # To backward compatibility return true if no artifacts found @@ -717,7 +717,7 @@ module Ci end def artifact_is_public_in_config? - return true if Feature.disabled?(:non_public_artifacts, type: :development) + return true if Feature.disabled?(:non_public_artifacts, project, type: :development) artifacts_public = options.dig(:artifacts, :public) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d3705265d91..0c6fcc02344 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -362,7 +362,7 @@ module Ci end def public_access? - return true unless Feature.enabled?(:non_public_artifacts, type: :development) + return true unless Feature.enabled?(:non_public_artifacts, project, type: :development) public_accessibility? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f771c264651..f132214ab7c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -97,11 +97,11 @@ module Ci has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' - has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' has_many :downloadable_artifacts, -> do not_expired.or(where_exists(Ci::Pipeline.artifacts_locked.where("#{Ci::Pipeline.quoted_table_name}.id = #{Ci::Build.quoted_table_name}.commit_id"))).downloadable.with_job end, through: :latest_builds, source: :job_artifacts - has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable' + has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable' has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index 5dcae7d594d..c0830d237c5 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -27,8 +27,8 @@ module Ci before_validation :ensure_metadata, on: :create - scope :with_project_and_metadata, -> do - if Feature.enabled?(:non_public_artifacts, type: :development) + scope :with_project_and_metadata, ->(project) do + if Feature.enabled?(:non_public_artifacts, project, type: :development) joins(:metadata).includes(:metadata).preload(:project) end end diff --git a/app/serializers/ci/downloadable_artifact_entity.rb b/app/serializers/ci/downloadable_artifact_entity.rb index 2e8aafcee43..c2caa371614 100644 --- a/app/serializers/ci/downloadable_artifact_entity.rb +++ b/app/serializers/ci/downloadable_artifact_entity.rb @@ -6,12 +6,13 @@ module Ci expose :artifacts do |pipeline, options| artifacts = pipeline.downloadable_artifacts + project = pipeline.project - if Feature.enabled?(:non_public_artifacts) + if Feature.enabled?(:non_public_artifacts, project) artifacts = artifacts.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } end - BuildArtifactEntity.represent(artifacts, options.merge(project: pipeline.project)) + BuildArtifactEntity.represent(artifacts, options.merge(project: project)) end end end diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb index 83f168682db..ef4debbd89f 100644 --- a/app/serializers/merge_requests/pipeline_entity.rb +++ b/app/serializers/merge_requests/pipeline_entity.rb @@ -25,12 +25,13 @@ class MergeRequests::PipelineEntity < Grape::Entity expose :artifacts do |pipeline, options| rel = pipeline.downloadable_artifacts + project = pipeline.project - if Feature.enabled?(:non_public_artifacts, type: :development) + if Feature.enabled?(:non_public_artifacts, project, type: :development) rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) } end - BuildArtifactEntity.represent(rel, options.merge(project: pipeline.project)) + BuildArtifactEntity.represent(rel, options.merge(project: project)) end expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline| diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb index b04e73c69ee..5927e9602b9 100644 --- a/app/services/ci/job_artifacts/create_service.rb +++ b/app/services/ci/job_artifacts/create_service.rb @@ -128,7 +128,7 @@ module Ci def accessibility(params) accessibility = params[:accessibility] - return :public if Feature.disabled?(:non_public_artifacts, type: :development) + return :public if Feature.disabled?(:non_public_artifacts, project, type: :development) return accessibility if accessibility.present? diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 1d2e6e1e332..c3d8da0f9a0 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -1,6 +1,8 @@ - link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5" .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between + = render_if_exists "dashboard/projects/blank_state_extra_info" + - if has_start_trial? = render_if_exists "dashboard/projects/blank_state_ee_trial" diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml index 032c5206d99..15048d29146 100644 --- a/app/views/dashboard/projects/_blank_state_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -1,6 +1,8 @@ - link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5" .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between + = render_if_exists "dashboard/projects/blank_state_extra_info" + - if current_user.can_create_project? = link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do .blank-state-icon diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d30ff5ce4e6..d8e78e28b55 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2636,7 +2636,7 @@ :feature_category: :importers :has_external_dependencies: false :urgency: :low - :resource_boundary: :unknown + :resource_boundary: :memory :weight: 1 :idempotent: true :tags: [] diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb index a9ffb2b252d..08c5fb81460 100644 --- a/app/workers/bulk_imports/relation_batch_export_worker.rb +++ b/app/workers/bulk_imports/relation_batch_export_worker.rb @@ -8,6 +8,7 @@ module BulkImports data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency feature_category :importers sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 6 + worker_resource_boundary :memory sidekiq_retries_exhausted do |job, exception| batch = BulkImports::ExportBatch.find(job['args'][1]) |