Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/code_navigation/components/doc_line.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue18
-rw-r--r--app/assets/javascripts/jobs/bridge/app.vue20
-rw-r--r--app/assets/javascripts/jobs/bridge/components/constants.js1
-rw-r--r--app/assets/javascripts/jobs/bridge/components/empty_state.vue45
-rw-r--r--app/assets/javascripts/jobs/bridge/components/sidebar.vue98
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/jobs/index.js39
-rw-r--r--app/assets/javascripts/milestones/milestone.js40
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue24
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js6
-rw-r--r--app/assets/javascripts/runner/components/runner_status_badge.vue2
-rw-r--r--app/assets/javascripts/runner/constants.js1
-rw-r--r--app/assets/javascripts/tabs/constants.js20
-rw-r--r--app/assets/javascripts/tabs/index.js239
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss35
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb4
-rw-r--r--app/controllers/groups/dependency_proxies_controller.rb19
-rw-r--r--app/controllers/projects/jobs_controller.rb4
-rw-r--r--app/controllers/user_callouts_controller.rb29
-rw-r--r--app/controllers/users/callouts_controller.rb31
-rw-r--r--app/controllers/users/group_callouts_controller.rb2
-rw-r--r--app/graphql/mutations/user_callouts/create.rb2
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb8
-rw-r--r--app/graphql/types/user_callout_feature_name_enum.rb2
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/ci/jobs_helper.rb7
-rw-r--r--app/helpers/ci/runners_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/tab_helper.rb5
-rw-r--r--app/helpers/user_callouts_helper.rb98
-rw-r--r--app/helpers/users/callouts_helper.rb76
-rw-r--r--app/helpers/users/group_callouts_helper.rb32
-rw-r--r--app/models/ci/runner.rb7
-rw-r--r--app/models/concerns/calloutable.rb15
-rw-r--r--app/models/user.rb4
-rw-r--r--app/models/user_callout.rb48
-rw-r--r--app/models/users/callout.rb52
-rw-r--r--app/models/users/calloutable.rb17
-rw-r--r--app/models/users/group_callout.rb2
-rw-r--r--app/presenters/blob_presenter.rb40
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/services/ci/retry_build_service.rb36
-rw-r--r--app/services/merge_requests/outdated_discussion_diff_lines_service.rb20
-rw-r--r--app/services/users/dismiss_callout_service.rb (renamed from app/services/users/dismiss_user_callout_service.rb)2
-rw-r--r--app/services/users/dismiss_group_callout_service.rb2
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/devise/shared/_tab_single.html.haml5
-rw-r--r--app/views/groups/registry/repositories/index.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/layouts/header/_registration_enabled_callout.html.haml2
-rw-r--r--app/views/projects/feature_flags/new.html.haml4
-rw-r--r--app/views/projects/jobs/show.html.haml5
-rw-r--r--app/views/projects/registry/repositories/index.html.haml4
-rw-r--r--app/views/root/index.html.haml4
-rw-r--r--app/views/shared/_flash_user_callout.html.haml2
-rw-r--r--app/views/shared/_two_factor_auth_recovery_settings_check.html.haml2
-rw-r--r--app/views/shared/milestones/_tabs.html.haml30
59 files changed, 864 insertions, 369 deletions
diff --git a/app/assets/javascripts/code_navigation/components/doc_line.vue b/app/assets/javascripts/code_navigation/components/doc_line.vue
index 69d398893d9..4d44c984833 100644
--- a/app/assets/javascripts/code_navigation/components/doc_line.vue
+++ b/app/assets/javascripts/code_navigation/components/doc_line.vue
@@ -18,5 +18,6 @@ export default {
<span v-for="(token, tokenIndex) in tokens" :key="tokenIndex" :class="token.class">{{
token.value
}}</span>
+ <br />
</span>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index ec6025c84bb..298771a4d12 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -170,7 +170,7 @@ export default {
},
availableGroupsForImport() {
- return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && g.flags.isInvalid);
+ return this.groupsTableData.filter((g) => g.flags.isAvailableForImport && !g.flags.isInvalid);
},
humanizedTotal() {
@@ -521,13 +521,15 @@ export default {
/>
<template v-else>
<div
- class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-p-4 gl-display-flex gl-align-items-center"
+ class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center import-table-bar"
>
- <gl-sprintf :message="__('%{count} selected')">
- <template #count>
- {{ selectedGroupsIds.length }}
- </template>
- </gl-sprintf>
+ <span data-test-id="selection-count">
+ <gl-sprintf :message="__('%{count} selected')">
+ <template #count>
+ {{ selectedGroupsIds.length }}
+ </template>
+ </gl-sprintf>
+ </span>
<gl-button
category="primary"
variant="confirm"
@@ -539,7 +541,7 @@ export default {
</div>
<gl-table
ref="table"
- class="gl-w-full"
+ class="gl-w-full import-table"
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue
new file mode 100644
index 00000000000..67c22712776
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/app.vue
@@ -0,0 +1,20 @@
+<script>
+import BridgeEmptyState from './components/empty_state.vue';
+import BridgeSidebar from './components/sidebar.vue';
+
+export default {
+ name: 'BridgePageApp',
+ components: {
+ BridgeEmptyState,
+ BridgeSidebar,
+ },
+};
+</script>
+<template>
+ <div>
+ <!-- TODO: get job details and show CI header -->
+ <!-- TODO: add downstream pipeline path -->
+ <bridge-empty-state downstream-pipeline-path="#" />
+ <bridge-sidebar />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/constants.js b/app/assets/javascripts/jobs/bridge/components/constants.js
new file mode 100644
index 00000000000..33310b3157a
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/constants.js
@@ -0,0 +1 @@
+export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm'];
diff --git a/app/assets/javascripts/jobs/bridge/components/empty_state.vue b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
new file mode 100644
index 00000000000..bd07d863719
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/empty_state.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ name: 'BridgeEmptyState',
+ i18n: {
+ title: __('This job triggers a downstream pipeline'),
+ linkBtnText: __('View downstream pipeline'),
+ },
+ components: {
+ GlButton,
+ },
+ inject: {
+ emptyStateIllustrationPath: {
+ type: String,
+ require: true,
+ },
+ },
+ props: {
+ downstreamPipelinePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <img :src="emptyStateIllustrationPath" />
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <gl-button
+ v-if="downstreamPipelinePath"
+ class="gl-mt-3"
+ category="secondary"
+ variant="confirm"
+ size="medium"
+ :href="downstreamPipelinePath"
+ >
+ {{ $options.i18n.linkBtnText }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
new file mode 100644
index 00000000000..68b767408f0
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { __ } from '~/locale';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
+import { JOB_SIDEBAR } from '../../constants';
+import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './constants';
+
+export default {
+ styles: {
+ top: '75px',
+ width: '290px',
+ },
+ name: 'BridgeSidebar',
+ i18n: {
+ ...JOB_SIDEBAR,
+ retryButton: __('Retry'),
+ retryTriggerJob: __('Retry the trigger job'),
+ retryDownstreamPipeline: __('Retry the downstream pipeline'),
+ },
+ borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ TooltipOnTruncate,
+ },
+ inject: {
+ buildName: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isSidebarExpanded: true,
+ };
+ },
+ created() {
+ window.addEventListener('resize', this.onResize);
+ },
+ mounted() {
+ this.onResize();
+ },
+ methods: {
+ toggleSidebar() {
+ this.isSidebarExpanded = !this.isSidebarExpanded;
+ },
+ onResize() {
+ const breakpoint = bp.getBreakpointSize();
+ if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
+ this.isSidebarExpanded = false;
+ } else if (!this.isSidebarExpanded) {
+ this.isSidebarExpanded = true;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <aside
+ class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden"
+ :style="this.$options.styles"
+ :class="{
+ 'gl-display-none': !isSidebarExpanded,
+ }"
+ >
+ <div class="gl-py-5 gl-display-flex gl-align-items-center">
+ <tooltip-on-truncate :title="buildName" truncate-target="child"
+ ><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate">
+ {{ buildName }}
+ </h4>
+ </tooltip-on-truncate>
+ <!-- TODO: implement retry actions -->
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-dropdown
+ :text="$options.i18n.retryButton"
+ category="primary"
+ variant="confirm"
+ right
+ size="medium"
+ >
+ <gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item>
+ <gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ <gl-button
+ :aria-label="$options.i18n.toggleSidebar"
+ data-testid="sidebar-expansion-toggle"
+ category="tertiary"
+ class="gl-md-display-none gl-ml-2"
+ icon="chevron-double-lg-right"
+ @click="toggleSidebar"
+ />
+ </div>
+ <!-- TODO: get job details and show commit block, stage dropdown, jobs list -->
+ </aside>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 6105299e15c..97141a27a5e 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -5,7 +5,7 @@ import { __, s__, sprintf } from '~/locale';
export default {
i18n: {
- eraseLogButtonLabel: s__('Job|Erase job log'),
+ eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
scrollToBottomButtonLabel: s__('Job|Scroll to bottom'),
scrollToTopButtonLabel: s__('Job|Scroll to top'),
showRawButtonLabel: s__('Job|Show complete raw'),
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 1fb6a6f9850..e078a6c2319 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import BridgeApp from './bridge/app.vue';
import JobApp from './components/job_app.vue';
import createStore from './store';
-export default () => {
- const element = document.getElementById('js-job-vue-app');
-
+const initializeJobPage = (element) => {
const store = createStore();
// Let's start initializing the store (i.e. fetching data) right away
@@ -51,3 +52,35 @@ export default () => {
},
});
};
+
+const initializeBridgePage = (el) => {
+ const { buildName, emptyStateIllustrationPath } = el.dataset;
+
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ buildName,
+ emptyStateIllustrationPath,
+ },
+ render(h) {
+ return h(BridgeApp);
+ },
+ });
+};
+
+export default () => {
+ const jobElement = document.getElementById('js-job-page');
+ const bridgeElement = document.getElementById('js-bridge-page');
+
+ if (jobElement) {
+ initializeJobPage(jobElement);
+ } else {
+ initializeBridgePage(bridgeElement);
+ }
+};
diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js
index 2c43bed412e..05102f73f92 100644
--- a/app/assets/javascripts/milestones/milestone.js
+++ b/app/assets/javascripts/milestones/milestone.js
@@ -1,43 +1,43 @@
-import $ from 'jquery';
import createFlash from '~/flash';
+import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
+import { historyPushState } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
export default class Milestone {
constructor() {
+ this.tabsEl = document.querySelector('.js-milestone-tabs');
+ this.glTabs = new GlTabsBehavior(this.tabsEl);
+ this.loadedTabs = new WeakSet();
+
this.bindTabsSwitching();
this.loadInitialTab();
}
bindTabsSwitching() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
-
- window.location.hash = $target.attr('href');
- this.loadTab($target);
+ this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => {
+ const tab = event.target;
+ const { activeTabPanel } = event.detail;
+ historyPushState(tab.getAttribute('href'));
+ this.loadTab(tab, activeTabPanel);
});
}
loadInitialTab() {
- const $target = $(`.js-milestone-tabs a:not(.active)[href="${window.location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- } else {
- this.loadTab($('.js-milestone-tabs a.active'));
- }
+ const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`);
+ this.glTabs.activateTab(tab || this.glTabs.activeTab);
}
- // eslint-disable-next-line class-methods-use-this
- loadTab($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
+ loadTab(tab, tabPanel) {
+ const { endpoint } = tab.dataset;
- if (endpoint && !$target.hasClass('is-loaded')) {
+ if (endpoint && !this.loadedTabs.has(tab)) {
axios
.get(endpoint)
.then(({ data }) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
+ // eslint-disable-next-line no-param-reassign
+ tabPanel.innerHTML = sanitize(data.html);
+ this.loadedTabs.add(tab);
})
.catch(() =>
createFlash({
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index 52c63a1355a..eb112238c11 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -4,7 +4,6 @@ import {
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
- GlLink,
GlSkeletonLoader,
GlSprintf,
} from '@gitlab/ui';
@@ -16,10 +15,7 @@ import {
DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
DEPENDENCY_PROXY_DOCS_PATH,
} from '~/packages_and_registries/settings/group/constants';
-import {
- GRAPHQL_PAGE_SIZE,
- ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
-} from '~/packages_and_registries/dependency_proxy/constants';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@@ -29,7 +25,6 @@ export default {
GlEmptyState,
GlFormGroup,
GlFormInputGroup,
- GlLink,
GlSkeletonLoader,
GlSprintf,
ClipboardButton,
@@ -41,9 +36,6 @@ export default {
proxyNotAvailableText: s__(
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
),
- proxyDisabledText: s__(
- 'DependencyProxy|The Dependency Proxy is disabled. %{docLinkStart}Learn how to enable it%{docLinkEnd}.',
- ),
proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'),
copyImagePrefixText: s__('DependencyProxy|Copy prefix'),
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
@@ -52,7 +44,6 @@ export default {
},
links: {
DEPENDENCY_PROXY_DOCS_PATH,
- ENABLE_DEPENDENCY_PROXY_DOCS_PATH,
},
data() {
return {
@@ -79,9 +70,7 @@ export default {
},
];
},
- dependencyProxyEnabled() {
- return this.group?.dependencyProxySetting?.enabled;
- },
+
queryVariables() {
return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
},
@@ -131,7 +120,7 @@ export default {
<gl-skeleton-loader v-else-if="$apollo.queries.group.loading" />
- <div v-else-if="dependencyProxyEnabled" data-testid="main-area">
+ <div v-else data-testid="main-area">
<gl-form-group :label="$options.i18n.proxyImagePrefix">
<gl-form-input-group
readonly
@@ -170,12 +159,5 @@ export default {
:title="$options.i18n.noManifestTitle"
/>
</div>
- <gl-alert v-else :dismissible="false" data-testid="proxy-disabled">
- <gl-sprintf :message="$options.i18n.proxyDisabledText">
- <template #docLink="{ content }">
- <gl-link :href="$options.links.ENABLE_DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
index ab1e3adcb55..3c6ede6fdce 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/constants.js
@@ -1,7 +1 @@
-import { helpPagePath } from '~/helpers/help_page_helper';
-
export const GRAPHQL_PAGE_SIZE = 20;
-export const ENABLE_DEPENDENCY_PROXY_DOCS_PATH = helpPagePath(
- 'user/packages/dependency_proxy/index',
- { anchor: 'enable-or-disable-the-dependency-proxy-for-a-group' },
-);
diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue
index 24ca6aacfc9..0823876a187 100644
--- a/app/assets/javascripts/runner/components/runner_status_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_status_badge.vue
@@ -9,6 +9,7 @@ import {
I18N_STALE_RUNNER_DESCRIPTION,
STATUS_ONLINE,
STATUS_NOT_CONNECTED,
+ STATUS_NEVER_CONTACTED,
STATUS_OFFLINE,
STATUS_STALE,
} from '../constants';
@@ -45,6 +46,7 @@ export default {
}),
};
case STATUS_NOT_CONNECTED:
+ case STATUS_NEVER_CONTACTED:
return {
variant: 'muted',
label: s__('Runners|not connected'),
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 68e45fcf8e9..355f3054917 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -61,6 +61,7 @@ export const STATUS_PAUSED = 'PAUSED';
export const STATUS_ONLINE = 'ONLINE';
export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
+export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
diff --git a/app/assets/javascripts/tabs/constants.js b/app/assets/javascripts/tabs/constants.js
new file mode 100644
index 00000000000..3b84d7394d4
--- /dev/null
+++ b/app/assets/javascripts/tabs/constants.js
@@ -0,0 +1,20 @@
+export const ACTIVE_TAB_CLASSES = Object.freeze([
+ 'active',
+ 'gl-tab-nav-item-active',
+ 'gl-tab-nav-item-active-indigo',
+]);
+
+export const ACTIVE_PANEL_CLASS = 'active';
+
+export const KEY_CODE_LEFT = 'ArrowLeft';
+export const KEY_CODE_UP = 'ArrowUp';
+export const KEY_CODE_RIGHT = 'ArrowRight';
+export const KEY_CODE_DOWN = 'ArrowDown';
+
+export const ATTR_ARIA_CONTROLS = 'aria-controls';
+export const ATTR_ARIA_LABELLEDBY = 'aria-labelledby';
+export const ATTR_ARIA_SELECTED = 'aria-selected';
+export const ATTR_ROLE = 'role';
+export const ATTR_TABINDEX = 'tabindex';
+
+export const TAB_SHOWN_EVENT = 'gl-tab-shown';
diff --git a/app/assets/javascripts/tabs/index.js b/app/assets/javascripts/tabs/index.js
new file mode 100644
index 00000000000..44937e593e0
--- /dev/null
+++ b/app/assets/javascripts/tabs/index.js
@@ -0,0 +1,239 @@
+import { uniqueId } from 'lodash';
+import {
+ ACTIVE_TAB_CLASSES,
+ ATTR_ROLE,
+ ATTR_ARIA_CONTROLS,
+ ATTR_TABINDEX,
+ ATTR_ARIA_SELECTED,
+ ATTR_ARIA_LABELLEDBY,
+ ACTIVE_PANEL_CLASS,
+ KEY_CODE_LEFT,
+ KEY_CODE_UP,
+ KEY_CODE_RIGHT,
+ KEY_CODE_DOWN,
+ TAB_SHOWN_EVENT,
+} from './constants';
+
+export { TAB_SHOWN_EVENT };
+
+/**
+ * The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and
+ * `gl_tab_link_to` Rails helpers.
+ *
+ * Example using `href` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#foo', item_active: true do
+ * = _('Foo')
+ * = gl_tab_link_to '#bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * Example using `aria-controls` references:
+ *
+ * ```haml
+ * = gl_tabs_nav({ class: 'js-my-tabs' }) do
+ * = gl_tab_link_to '#', item_active: true, 'aria-controls': 'foo' do
+ * = _('Foo')
+ * = gl_tab_link_to '#', 'aria-controls': 'bar' do
+ * = _('Bar')
+ *
+ * .tab-content
+ * .tab-pane.active#foo
+ * .tab-pane#bar
+ * ```
+ *
+ * ```javascript
+ * import { GlTabsBehavior } from '~/tabs';
+ *
+ * const glTabs = new GlTabsBehavior(document.querySelector('.js-my-tabs'));
+ * ```
+ *
+ * `GlTabsBehavior` can be used to replace Bootstrap tab implementations that cannot
+ * easily be rewritten in Vue.
+ *
+ * NOTE: Do *not* use `GlTabsBehavior` with markup generated by other means, as it may not
+ * work correctly.
+ *
+ * Tab panels must exist somewhere in the page for the tabs to control. Tab panels
+ * must:
+ * - be immediate children of a `.tab-content` element
+ * - have the `tab-pane` class
+ * - if the panel is active, have the `active` class
+ * - have a unique `id` attribute
+ *
+ * In order to associate tabs with panels, the tabs must reference their panel's
+ * `id` by having one of the following attributes:
+ * - `href`, e.g., `href="#the-panel-id"` (note the leading `#` in the value)
+ * - `aria-controls`, e.g., `aria-controls="the-panel-id"` (no leading `#`)
+ *
+ * Exactly one tab/panel must be active in the original markup.
+ *
+ * Call the `destroy` method on an instance to remove event listeners that were
+ * added during construction. Other DOM mutations (like ARIA attributes) are
+ * _not_ reverted.
+ */
+export class GlTabsBehavior {
+ /**
+ * Create a GlTabsBehavior instance.
+ *
+ * @param {HTMLElement} el The element created by the Rails `gl_tabs_nav` helper.
+ */
+ constructor(el) {
+ if (!el) {
+ throw new Error('Cannot instantiate GlTabsBehavior without an element');
+ }
+
+ this.destroyFns = [];
+ this.tabList = el;
+ this.tabs = this.getTabs();
+ this.activeTab = null;
+
+ this.setAccessibilityAttrs();
+ this.bindEvents();
+ }
+
+ setAccessibilityAttrs() {
+ this.tabList.setAttribute(ATTR_ROLE, 'tablist');
+ this.tabs.forEach((tab) => {
+ if (!tab.hasAttribute('id')) {
+ tab.setAttribute('id', uniqueId('gl_tab_nav__tab_'));
+ }
+
+ if (!this.activeTab && tab.classList.contains(ACTIVE_TAB_CLASSES[0])) {
+ this.activeTab = tab;
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tab.removeAttribute(ATTR_TABINDEX);
+ } else {
+ tab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ tab.setAttribute(ATTR_TABINDEX, '-1');
+ }
+
+ tab.setAttribute(ATTR_ROLE, 'tab');
+ tab.closest('.nav-item').setAttribute(ATTR_ROLE, 'presentation');
+
+ const tabPanel = this.getPanelForTab(tab);
+ if (!tab.hasAttribute(ATTR_ARIA_CONTROLS)) {
+ tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id);
+ }
+
+ tabPanel.setAttribute(ATTR_ROLE, 'tabpanel');
+ tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id);
+ });
+ }
+
+ bindEvents() {
+ this.tabs.forEach((tab) => {
+ this.bindEvent(tab, 'click', (event) => {
+ event.preventDefault();
+
+ if (tab !== this.activeTab) {
+ this.activateTab(tab);
+ }
+ });
+
+ this.bindEvent(tab, 'keydown', (event) => {
+ const { code } = event;
+ if (code === KEY_CODE_UP || code === KEY_CODE_LEFT) {
+ event.preventDefault();
+ this.activatePreviousTab();
+ } else if (code === KEY_CODE_DOWN || code === KEY_CODE_RIGHT) {
+ event.preventDefault();
+ this.activateNextTab();
+ }
+ });
+ });
+ }
+
+ bindEvent(el, ...args) {
+ el.addEventListener(...args);
+
+ this.destroyFns.push(() => {
+ el.removeEventListener(...args);
+ });
+ }
+
+ activatePreviousTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex <= 0) return;
+
+ const previousTab = this.tabs[currentTabIndex - 1];
+ this.activateTab(previousTab);
+ previousTab.focus();
+ }
+
+ activateNextTab() {
+ const currentTabIndex = this.tabs.indexOf(this.activeTab);
+
+ if (currentTabIndex >= this.tabs.length - 1) return;
+
+ const nextTab = this.tabs[currentTabIndex + 1];
+ this.activateTab(nextTab);
+ nextTab.focus();
+ }
+
+ getTabs() {
+ return Array.from(this.tabList.querySelectorAll('.gl-tab-nav-item'));
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getPanelForTab(tab) {
+ const ariaControls = tab.getAttribute(ATTR_ARIA_CONTROLS);
+
+ if (ariaControls) {
+ return document.querySelector(`#${ariaControls}`);
+ }
+
+ return document.querySelector(tab.getAttribute('href'));
+ }
+
+ activateTab(tabToActivate) {
+ // Deactivate active tab first
+ this.activeTab.setAttribute(ATTR_ARIA_SELECTED, 'false');
+ this.activeTab.setAttribute(ATTR_TABINDEX, '-1');
+ this.activeTab.classList.remove(...ACTIVE_TAB_CLASSES);
+
+ const activePanel = this.getPanelForTab(this.activeTab);
+ activePanel.classList.remove(ACTIVE_PANEL_CLASS);
+
+ // Now activate the given tab/panel
+ tabToActivate.setAttribute(ATTR_ARIA_SELECTED, 'true');
+ tabToActivate.removeAttribute(ATTR_TABINDEX);
+ tabToActivate.classList.add(...ACTIVE_TAB_CLASSES);
+
+ const tabPanel = this.getPanelForTab(tabToActivate);
+ tabPanel.classList.add(ACTIVE_PANEL_CLASS);
+
+ this.activeTab = tabToActivate;
+
+ this.dispatchTabShown(tabToActivate, tabPanel);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ dispatchTabShown(tab, activeTabPanel) {
+ const event = new CustomEvent(TAB_SHOWN_EVENT, {
+ bubbles: true,
+ detail: {
+ activeTabPanel,
+ },
+ });
+
+ tab.dispatchEvent(event);
+ }
+
+ destroy() {
+ this.destroyFns.forEach((destroy) => destroy());
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index c74b5460e1a..79468ce62ce 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -1,12 +1,5 @@
@import 'mixins_and_variables_and_functions';
-// Fixing double scrollbar issue
-// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1156 and
-// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54837
-.import-entities-namespace-dropdown.show.dropdown .dropdown-menu {
- max-height: initial;
-}
-
.import-jobs-to-col {
width: 39%;
}
@@ -38,3 +31,31 @@
box-shadow: inset 0 0 0 1px var(--gray-200, $gray-200);
}
}
+
+$import-bar-height: $gl-spacing-scale-11;
+
+.import-table-bar {
+ @include gl-sticky;
+ height: $import-bar-height;
+ top: $header-height;
+ z-index: 3;
+
+ html.with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+}
+
+.import-table {
+ border-collapse: separate;
+
+ thead {
+ @include gl-sticky;
+ background-color: var(--gray-10, $gray-10);
+ top: calc(#{$header-height} + #{$import-bar-height});
+ z-index: 3;
+
+ html.with-performance-bar & {
+ top: calc(#{$header-height + $performance-bar-height} + #{$import-bar-height});
+ }
+ }
+}
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
index 07aca72b22f..44611641529 100644
--- a/app/controllers/concerns/dependency_proxy/group_access.rb
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -5,13 +5,13 @@ module DependencyProxy
extend ActiveSupport::Concern
included do
- before_action :verify_dependency_proxy_enabled!
+ before_action :verify_dependency_proxy_available!
before_action :authorize_read_dependency_proxy!
end
private
- def verify_dependency_proxy_enabled!
+ def verify_dependency_proxy_available!
render_404 unless group&.dependency_proxy_feature_available?
end
diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb
index b037aa52939..2e120de435e 100644
--- a/app/controllers/groups/dependency_proxies_controller.rb
+++ b/app/controllers/groups/dependency_proxies_controller.rb
@@ -5,30 +5,19 @@ module Groups
include ::DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
- before_action :dependency_proxy
+ before_action :verify_dependency_proxy_enabled!
feature_category :package_registry
- def show
- @blobs_count = group.dependency_proxy_blobs.count
- @blobs_total_size = group.dependency_proxy_blobs.total_size
- end
-
- def update
- dependency_proxy.update(dependency_proxy_params)
-
- redirect_to group_dependency_proxy_path(group)
- end
-
private
def dependency_proxy
@dependency_proxy ||=
- group.dependency_proxy_setting || group.create_dependency_proxy_setting
+ group.dependency_proxy_setting || group.create_dependency_proxy_setting!
end
- def dependency_proxy_params
- params.require(:dependency_proxy_group_setting).permit(:enabled)
+ def verify_dependency_proxy_enabled!
+ render_404 unless dependency_proxy.enabled?
end
end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 81b8da9cba3..32a192192bd 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,8 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
include ContinueParams
- before_action :find_job_as_build, except: [:index, :play]
- before_action :find_job_as_processable, only: [:play]
+ before_action :find_job_as_build, except: [:index, :play, :show]
+ before_action :find_job_as_processable, only: [:play, :show]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
before_action :authorize_read_build!
before_action :authorize_update_build!,
diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb
deleted file mode 100644
index f52a09adf5a..00000000000
--- a/app/controllers/user_callouts_controller.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-class UserCalloutsController < ApplicationController
- feature_category :navigation
-
- def create
- if callout.persisted?
- respond_to do |format|
- format.json { head :ok }
- end
- else
- respond_to do |format|
- format.json { head :bad_request }
- end
- end
- end
-
- private
-
- def callout
- Users::DismissUserCalloutService.new(
- container: nil, current_user: current_user, params: { feature_name: feature_name }
- ).execute
- end
-
- def feature_name
- params.require(:feature_name)
- end
-end
diff --git a/app/controllers/users/callouts_controller.rb b/app/controllers/users/callouts_controller.rb
new file mode 100644
index 00000000000..fe308d9dd1e
--- /dev/null
+++ b/app/controllers/users/callouts_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Users
+ class CalloutsController < ApplicationController
+ feature_category :navigation
+
+ def create
+ if callout.persisted?
+ respond_to do |format|
+ format.json { head :ok }
+ end
+ else
+ respond_to do |format|
+ format.json { head :bad_request }
+ end
+ end
+ end
+
+ private
+
+ def callout
+ Users::DismissCalloutService.new(
+ container: nil, current_user: current_user, params: { feature_name: feature_name }
+ ).execute
+ end
+
+ def feature_name
+ params.require(:feature_name)
+ end
+ end
+end
diff --git a/app/controllers/users/group_callouts_controller.rb b/app/controllers/users/group_callouts_controller.rb
index cc27452e6a3..abca12ccea7 100644
--- a/app/controllers/users/group_callouts_controller.rb
+++ b/app/controllers/users/group_callouts_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class GroupCalloutsController < UserCalloutsController
+ class GroupCalloutsController < Users::CalloutsController
private
def callout
diff --git a/app/graphql/mutations/user_callouts/create.rb b/app/graphql/mutations/user_callouts/create.rb
index ff6e5cd28dd..1be99ea0ecd 100644
--- a/app/graphql/mutations/user_callouts/create.rb
+++ b/app/graphql/mutations/user_callouts/create.rb
@@ -15,7 +15,7 @@ module Mutations
description: 'User callout dismissed.'
def resolve(feature_name:)
- callout = Users::DismissUserCalloutService.new(
+ callout = Users::DismissCalloutService.new(
container: nil, current_user: current_user, params: { feature_name: feature_name }
).execute
errors = errors_on_object(callout)
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
index 14eae1cdce5..dd056191ceb 100644
--- a/app/graphql/types/ci/runner_status_enum.rb
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -25,13 +25,17 @@ module Types
value: :offline
value 'STALE',
- description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0",
+ description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0.",
value: :stale
value 'NOT_CONNECTED',
description: 'Runner that has never contacted this instance.',
- deprecated: { reason: 'This field will have a slightly different scope starting in 15.0, with STALE being returned after a certain period of no contact', milestone: '14.6' },
+ deprecated: { reason: "Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after #{::Ci::Runner::STALE_TIMEOUT.inspect} of no contact", milestone: '14.6' },
value: :not_connected
+
+ value 'NEVER_CONTACTED',
+ description: 'Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0.',
+ value: :never_contacted
end
end
end
diff --git a/app/graphql/types/user_callout_feature_name_enum.rb b/app/graphql/types/user_callout_feature_name_enum.rb
index 410ca5e1c95..bcb49a709ed 100644
--- a/app/graphql/types/user_callout_feature_name_enum.rb
+++ b/app/graphql/types/user_callout_feature_name_enum.rb
@@ -5,7 +5,7 @@ module Types
graphql_name 'UserCalloutFeatureNameEnum'
description 'Name of the feature that the callout is for.'
- ::UserCallout.feature_names.keys.each do |feature_name|
+ ::Users::Callout.feature_names.keys.each do |feature_name|
value feature_name.upcase, value: feature_name, description: "Callout feature name for #{feature_name}."
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 58f933a7fe0..02a87979f40 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -206,10 +206,6 @@ module ApplicationHelper
'https://' + promo_host
end
- def contact_sales_url
- promo_url + '/sales'
- end
-
def support_url
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index d02fe3f20b0..c7f40decae8 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -19,6 +19,13 @@ module Ci
}
end
+ def bridge_data(build)
+ {
+ "build_name" => build.name,
+ "empty-state-illustration-path" => image_path('illustrations/job-trigger-md.svg')
+ }
+ end
+
def job_counts
{
"all" => limited_counter_with_delimiter(@all_builds),
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 17057505173..8f219656b71 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -23,7 +23,7 @@ module Ci
icon = 'status-paused'
span_class = 'gl-text-gray-600'
end
- when :not_connected
+ when :not_connected, :never_contacted
title = s_("Runners|New runner, has not connected yet")
icon = 'warning-solid'
when :offline
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index d5d692f2d6e..abb7128470f 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -182,7 +182,7 @@ module MergeRequestsHelper
project_path: project_path(merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'),
is_fluid_layout: fluid_layout.to_s,
- dismiss_endpoint: user_callouts_path,
+ dismiss_endpoint: callouts_path,
show_suggest_popover: show_suggest_popover?.to_s,
show_whitespace_default: @show_whitespace_default.to_s,
file_by_file_default: @file_by_file_default.to_s,
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 3d51ba30c62..2efc3f27dc7 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -14,8 +14,7 @@ module TabHelper
gl_tabs_classes = %w[nav gl-tabs-nav]
html_options = html_options.merge(
- class: [*html_options[:class], gl_tabs_classes].join(' '),
- role: 'tablist'
+ class: [*html_options[:class], gl_tabs_classes].join(' ')
)
content = capture(&block) if block_given?
@@ -54,7 +53,7 @@ module TabHelper
extra_tab_classes = html_options.delete(:tab_class)
tab_class = %w[nav-item].push(*extra_tab_classes)
- content_tag(:li, class: tab_class, role: 'presentation') do
+ content_tag(:li, class: tab_class) do
if block_given?
link_to(options, html_options, &block)
else
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
deleted file mode 100644
index d8e69145c40..00000000000
--- a/app/helpers/user_callouts_helper.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-module UserCalloutsHelper
- GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
- GCP_SIGNUP_OFFER = 'gcp_signup_offer'
- SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
- TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
- CUSTOMIZE_HOMEPAGE = 'customize_homepage'
- FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
- REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
- UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
- INVITE_MEMBERS_BANNER = 'invite_members_banner'
- SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
-
- def show_gke_cluster_integration_callout?(project)
- active_nav_link?(controller: sidebar_operations_paths) &&
- can?(current_user, :create_cluster, project) &&
- !user_dismissed?(GKE_CLUSTER_INTEGRATION)
- end
-
- def show_gcp_signup_offer?
- !user_dismissed?(GCP_SIGNUP_OFFER)
- end
-
- def render_flash_user_callout(flash_type, message, feature_name)
- render 'shared/flash_user_callout', flash_type: flash_type, message: message, feature_name: feature_name
- end
-
- def render_dashboard_ultimate_trial(user)
- end
-
- def render_two_factor_auth_recovery_settings_check
- end
-
- def show_suggest_popover?
- !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
- end
-
- def show_customize_homepage_banner?
- current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
- end
-
- def show_feature_flags_new_version?
- !user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
- end
-
- def show_unfinished_tag_cleanup_callout?
- !user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT)
- end
-
- def show_registration_enabled_user_callout?
- !Gitlab.com? &&
- current_user&.admin? &&
- signup_enabled? &&
- !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
- end
-
- def dismiss_two_factor_auth_recovery_settings_check
- end
-
- def show_invite_banner?(group)
- Ability.allowed?(current_user, :admin_group, group) &&
- !just_created? &&
- !user_dismissed_for_group(INVITE_MEMBERS_BANNER, group) &&
- !multiple_members?(group)
- end
-
- def show_security_newsletter_user_callout?
- current_user&.admin? &&
- !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
- end
-
- private
-
- def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
- return false unless current_user
-
- current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
- end
-
- def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
- return false unless current_user
-
- current_user.dismissed_callout_for_group?(feature_name: feature_name,
- group: group,
- ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
- end
-
- def just_created?
- flash[:notice]&.include?('successfully created')
- end
-
- def multiple_members?(group)
- group.member_count > 1 || group.members_with_parents.count > 1
- end
-end
-
-UserCalloutsHelper.prepend_mod
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
new file mode 100644
index 00000000000..5ed17357e9b
--- /dev/null
+++ b/app/helpers/users/callouts_helper.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Users
+ module CalloutsHelper
+ GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
+ TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
+ CUSTOMIZE_HOMEPAGE = 'customize_homepage'
+ FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
+ REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
+ UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
+ SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
+
+ def show_gke_cluster_integration_callout?(project)
+ active_nav_link?(controller: sidebar_operations_paths) &&
+ can?(current_user, :create_cluster, project) &&
+ !user_dismissed?(GKE_CLUSTER_INTEGRATION)
+ end
+
+ def show_gcp_signup_offer?
+ !user_dismissed?(GCP_SIGNUP_OFFER)
+ end
+
+ def render_flash_user_callout(flash_type, message, feature_name)
+ render 'shared/flash_user_callout', flash_type: flash_type, message: message, feature_name: feature_name
+ end
+
+ def render_dashboard_ultimate_trial(user)
+ end
+
+ def render_two_factor_auth_recovery_settings_check
+ end
+
+ def show_suggest_popover?
+ !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
+ end
+
+ def show_customize_homepage_banner?
+ current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
+ end
+
+ def show_feature_flags_new_version?
+ !user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
+ end
+
+ def show_unfinished_tag_cleanup_callout?
+ !user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT)
+ end
+
+ def show_registration_enabled_user_callout?
+ !Gitlab.com? &&
+ current_user&.admin? &&
+ signup_enabled? &&
+ !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
+ end
+
+ def dismiss_two_factor_auth_recovery_settings_check
+ end
+
+ def show_security_newsletter_user_callout?
+ current_user&.admin? &&
+ !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
+ end
+
+ private
+
+ def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
+ return false unless current_user
+
+ current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
+ end
+ end
+end
+
+Users::CalloutsHelper.prepend_mod
diff --git a/app/helpers/users/group_callouts_helper.rb b/app/helpers/users/group_callouts_helper.rb
new file mode 100644
index 00000000000..b66c7f9f821
--- /dev/null
+++ b/app/helpers/users/group_callouts_helper.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Users
+ module GroupCalloutsHelper
+ INVITE_MEMBERS_BANNER = 'invite_members_banner'
+
+ def show_invite_banner?(group)
+ Ability.allowed?(current_user, :admin_group, group) &&
+ !just_created? &&
+ !user_dismissed_for_group(INVITE_MEMBERS_BANNER, group) &&
+ !multiple_members?(group)
+ end
+
+ private
+
+ def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
+ return false unless current_user
+
+ current_user.dismissed_callout_for_group?(feature_name: feature_name,
+ group: group,
+ ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
+ end
+
+ def just_created?
+ flash[:notice]&.include?('successfully created')
+ end
+
+ def multiple_members?(group)
+ group.member_count > 1 || group.members_with_parents.count > 1
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 3ede3ef3347..a441d362b74 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -44,7 +44,7 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline not_connected stale].freeze
+ AVAILABLE_STATUSES = %w[active paused online offline not_connected never_contacted stale].freeze # TODO: Remove in %15.0: active, paused, not_connected. Relevant issues: https://gitlab.com/gitlab-org/gitlab/-/issues/347303, https://gitlab.com/gitlab-org/gitlab/-/issues/347305, https://gitlab.com/gitlab-org/gitlab/-/issues/344648
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
@@ -66,7 +66,8 @@ module Ci
scope :recent, -> { where('ci_runners.created_at >= :date OR ci_runners.contacted_at >= :date', date: stale_deadline) }
scope :stale, -> { where('ci_runners.created_at < :date AND (ci_runners.contacted_at IS NULL OR ci_runners.contacted_at < :date)', date: stale_deadline) }
scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
- scope :not_connected, -> { where(contacted_at: nil) }
+ scope :not_connected, -> { where(contacted_at: nil) } # TODO: Remove in 15.0
+ scope :never_contacted, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
@@ -284,7 +285,7 @@ module Ci
return deprecated_rest_status if legacy_mode == '14.5'
return :stale if stale?
- return :not_connected unless contacted_at
+ return :never_contacted unless contacted_at
online? ? :online : :offline
end
diff --git a/app/models/concerns/calloutable.rb b/app/models/concerns/calloutable.rb
deleted file mode 100644
index 8b9cfae6a32..00000000000
--- a/app/models/concerns/calloutable.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Calloutable
- extend ActiveSupport::Concern
-
- included do
- belongs_to :user
-
- validates :user, presence: true
- end
-
- def dismissed_after?(dismissed_after)
- dismissed_at > dismissed_after
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index f8579add392..98d2ceb6dbe 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -204,7 +204,7 @@ class User < ApplicationRecord
has_many :bulk_imports
has_many :custom_attributes, class_name: 'UserCustomAttribute'
- has_many :callouts, class_name: 'UserCallout'
+ has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -1947,7 +1947,7 @@ class User < ApplicationRecord
end
def find_or_initialize_callout(feature_name)
- callouts.find_or_initialize_by(feature_name: ::UserCallout.feature_names[feature_name])
+ callouts.find_or_initialize_by(feature_name: ::Users::Callout.feature_names[feature_name])
end
def find_or_initialize_group_callout(feature_name, group_id)
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
deleted file mode 100644
index 5956c82384e..00000000000
--- a/app/models/user_callout.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-class UserCallout < ApplicationRecord
- include Calloutable
-
- enum feature_name: {
- gke_cluster_integration: 1,
- gcp_signup_offer: 2,
- cluster_security_warning: 3,
- ultimate_trial: 4, # EE-only
- geo_enable_hashed_storage: 5, # EE-only
- geo_migrate_hashed_storage: 6, # EE-only
- canary_deployment: 7, # EE-only
- gold_trial_billings: 8, # EE-only
- suggest_popover_dismissed: 9,
- tabs_position_highlight: 10,
- threat_monitoring_info: 11, # EE-only
- two_factor_auth_recovery_settings_check: 12, # EE-only
- web_ide_alert_dismissed: 16, # no longer in use
- active_user_count_threshold: 18, # EE-only
- buy_pipeline_minutes_notification_dot: 19, # EE-only
- personal_access_token_expiry: 21, # EE-only
- suggest_pipeline: 22,
- customize_homepage: 23,
- feature_flags_new_version: 24,
- registration_enabled_callout: 25,
- new_user_signups_cap_reached: 26, # EE-only
- unfinished_tag_cleanup_callout: 27,
- eoa_bronze_plan_banner: 28, # EE-only
- pipeline_needs_banner: 29,
- pipeline_needs_hover_tip: 30,
- web_ide_ci_environments_guidance: 31,
- security_configuration_upgrade_banner: 32,
- cloud_licensing_subscription_activation_banner: 33, # EE-only
- trial_status_reminder_d14: 34, # EE-only
- trial_status_reminder_d3: 35, # EE-only
- security_configuration_devops_alert: 36, # EE-only
- profile_personal_access_token_expiry: 37, # EE-only
- terraform_notification_dismissed: 38,
- security_newsletter_callout: 39,
- verification_reminder: 40 # EE-only
- }
-
- validates :feature_name,
- presence: true,
- uniqueness: { scope: :user_id },
- inclusion: { in: UserCallout.feature_names.keys }
-end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
new file mode 100644
index 00000000000..9a729072051
--- /dev/null
+++ b/app/models/users/callout.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Users
+ class Callout < ApplicationRecord
+ include Users::Calloutable
+
+ self.table_name = 'user_callouts'
+
+ enum feature_name: {
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2,
+ cluster_security_warning: 3,
+ ultimate_trial: 4, # EE-only
+ geo_enable_hashed_storage: 5, # EE-only
+ geo_migrate_hashed_storage: 6, # EE-only
+ canary_deployment: 7, # EE-only
+ gold_trial_billings: 8, # EE-only
+ suggest_popover_dismissed: 9,
+ tabs_position_highlight: 10,
+ threat_monitoring_info: 11, # EE-only
+ two_factor_auth_recovery_settings_check: 12, # EE-only
+ web_ide_alert_dismissed: 16, # no longer in use
+ active_user_count_threshold: 18, # EE-only
+ buy_pipeline_minutes_notification_dot: 19, # EE-only
+ personal_access_token_expiry: 21, # EE-only
+ suggest_pipeline: 22,
+ customize_homepage: 23,
+ feature_flags_new_version: 24,
+ registration_enabled_callout: 25,
+ new_user_signups_cap_reached: 26, # EE-only
+ unfinished_tag_cleanup_callout: 27,
+ eoa_bronze_plan_banner: 28, # EE-only
+ pipeline_needs_banner: 29,
+ pipeline_needs_hover_tip: 30,
+ web_ide_ci_environments_guidance: 31,
+ security_configuration_upgrade_banner: 32,
+ cloud_licensing_subscription_activation_banner: 33, # EE-only
+ trial_status_reminder_d14: 34, # EE-only
+ trial_status_reminder_d3: 35, # EE-only
+ security_configuration_devops_alert: 36, # EE-only
+ profile_personal_access_token_expiry: 37, # EE-only
+ terraform_notification_dismissed: 38,
+ security_newsletter_callout: 39,
+ verification_reminder: 40 # EE-only
+ }
+
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: :user_id },
+ inclusion: { in: Users::Callout.feature_names.keys }
+ end
+end
diff --git a/app/models/users/calloutable.rb b/app/models/users/calloutable.rb
new file mode 100644
index 00000000000..280a819e4d5
--- /dev/null
+++ b/app/models/users/calloutable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ module Calloutable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :user
+
+ validates :user, presence: true
+ end
+
+ def dismissed_after?(dismissed_after)
+ dismissed_at > dismissed_after
+ end
+ end
+end
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 540d1a1d242..da9b95fd718 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -2,7 +2,7 @@
module Users
class GroupCallout < ApplicationRecord
- include Calloutable
+ include Users::Calloutable
self.table_name = 'user_group_callouts'
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index b310f8fff15..3bd92ebc942 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -15,19 +15,8 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
Gitlab::Highlight.highlight(
blob.path,
- limited_blob_data(to: to),
- language: language,
- plain: plain
- )
- end
-
- def highlight_transformed(plain: nil)
- load_all_blob_data
-
- Gitlab::Highlight.highlight(
- blob.path,
- transformed_blob_data,
- language: transformed_blob_language,
+ blob_data(to),
+ language: blob_language,
plain: plain
)
end
@@ -38,6 +27,14 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
highlight(plain: false)
end
+ def blob_data(to)
+ @_blob_data ||= Gitlab::Diff::CustomDiff.transformed_blob_data(blob) || limited_blob_data(to: to)
+ end
+
+ def blob_language
+ @_blob_language ||= Gitlab::Diff::CustomDiff.transformed_blob_language(blob) || language
+ end
+
def raw_plain_data
blob.data unless blob.binary?
end
@@ -134,23 +131,6 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
def language
blob.language_from_gitattributes
end
-
- def transformed_blob_language
- @transformed_blob_language ||= blob.path.ends_with?('.ipynb') ? 'md' : language
- end
-
- def transformed_blob_data
- @transformed_blob ||= if blob.path.ends_with?('.ipynb') && blob.transformed_for_diff
- IpynbDiff.transform(blob.data,
- raise_errors: true,
- options: { include_metadata: false, cell_decorator: :percent })
- end
-
- @transformed_blob ||= blob.data
- rescue IpynbDiff::InvalidNotebookError => e
- Gitlab::ErrorTracking.log_exception(e)
- blob.data
- end
end
BlobPresenter.prepend_mod_with('BlobPresenter')
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index bd60d60c8db..b9c71e6d97b 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -73,7 +73,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :user_callouts_path do |_merge_request|
- user_callouts_path
+ callouts_path
end
expose :suggest_pipeline_feature_id do |_merge_request|
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index be21ed5b73d..89fe4ff9f60 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -2,6 +2,8 @@
module Ci
class RetryBuildService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def self.clone_accessors
%i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request
@@ -45,6 +47,11 @@ module Ci
job.save!
end
end
+
+ if create_deployment_in_separate_transaction?
+ clone_deployment!(new_build, build)
+ end
+
build.reset # refresh the data to get new values of `retried` and `processed`.
new_build
@@ -63,7 +70,9 @@ module Ci
def clone_build(build)
project.builds.new(build_attributes(build)).tap do |new_build|
- new_build.assign_attributes(deployment_attributes_for(new_build, build))
+ unless create_deployment_in_separate_transaction?
+ new_build.assign_attributes(deployment_attributes_for(new_build, build))
+ end
end
end
@@ -72,6 +81,11 @@ module Ci
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end
+ if create_deployment_in_separate_transaction? && build.persisted_environment.present?
+ attributes[:metadata_attributes] ||= {}
+ attributes[:metadata_attributes][:expanded_environment_name] = build.expanded_environment_name
+ end
+
attributes[:user] = current_user
attributes
end
@@ -80,6 +94,26 @@ module Ci
::Gitlab::Ci::Pipeline::Seed::Build
.deployment_attributes_for(new_build, old_build.persisted_environment)
end
+
+ def clone_deployment!(new_build, old_build)
+ return unless old_build.deployment.present?
+
+ # We should clone the previous deployment attributes instead of initializing
+ # new object with `Seed::Deployment`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/347206
+ deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment
+ .new(new_build, old_build.persisted_environment).to_resource
+
+ return unless deployment
+
+ new_build.create_deployment!(deployment.attributes)
+ end
+
+ def create_deployment_in_separate_transaction?
+ strong_memoize(:create_deployment_in_separate_transaction) do
+ ::Feature.enabled?(:create_deployment_in_separate_transaction, project, default_enabled: :yaml)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
index ad65a9afa6b..a3d94e888df 100644
--- a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
+++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
@@ -15,12 +15,22 @@ module MergeRequests
def execute
line_position = position.line_range["end"] || position.line_range["start"]
- diff_line_index = diff_lines.find_index do |l|
- if line_position["new_line"]
- l.new_line == line_position["new_line"]
- elsif line_position["old_line"]
- l.old_line == line_position["old_line"]
+ found_line = false
+ diff_line_index = -1
+ diff_lines.each_with_index do |l, i|
+ if found_line
+ if !l.type
+ break
+ elsif l.type == 'new'
+ diff_line_index = i
+ break
+ end
+ else
+ # Find the old line
+ found_line = l.old_line == line_position["new_line"]
end
+
+ diff_line_index = i
end
initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
diff --git a/app/services/users/dismiss_user_callout_service.rb b/app/services/users/dismiss_callout_service.rb
index 96f3f3acb57..4324e6232c2 100644
--- a/app/services/users/dismiss_user_callout_service.rb
+++ b/app/services/users/dismiss_callout_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class DismissUserCalloutService < BaseContainerService
+ class DismissCalloutService < BaseContainerService
def execute
callout.tap do |record|
record.update(dismissed_at: Time.current) if record.valid?
diff --git a/app/services/users/dismiss_group_callout_service.rb b/app/services/users/dismiss_group_callout_service.rb
index 8afee6a8187..f482142b911 100644
--- a/app/services/users/dismiss_group_callout_service.rb
+++ b/app/services/users/dismiss_group_callout_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
- class DismissGroupCalloutService < DismissUserCalloutService
+ class DismissGroupCalloutService < DismissCalloutService
private
def callout
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
index ece0f7ca4d9..3aba91e8765 100644
--- a/app/views/admin/dashboard/_security_newsletter_callout.html.haml
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -5,7 +5,7 @@
variant: :tip,
alert_class: 'js-security-newsletter-callout',
is_contained: true,
- alert_data: { feature_id: UserCalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ alert_data: { feature_id: Users::CalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-security-newsletter-callout' } do
.gl-alert-body
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 81f4be9fce5..9d249931a34 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,5 +1,5 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.gcp-signup-offer.gl-alert.gl-alert-info.gl-my-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
+.gcp-signup-offer.gl-alert.gl-alert-info.gl-my-3{ role: 'alert', data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path } }
.gl-alert-container
%button.js-close.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index 5683b4207b4..1b5a932a09a 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,2 @@
-%ul.nav-links.new-session-tabs.single-tab.nav-tabs.nav
- %li.nav-item
- %a.nav-link.active= tab_title
+= gl_tabs_nav({ class: 'new-session-tabs gl-border-0' }) do
+ = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1' }
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 2901c8fa46b..f6d05959d2e 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -18,6 +18,6 @@
"gid_prefix": container_repository_gid_prefix,
connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s,
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s } }
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index ed3f2b0c6db..bb409190dd8 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -15,7 +15,7 @@
track_label: 'invite_members_banner',
invite_members_path: group_group_members_path(@group),
callouts_path: group_callouts_path,
- callouts_feature_id: UserCalloutsHelper::INVITE_MEMBERS_BANNER,
+ callouts_feature_id: Users::GroupCalloutsHelper::INVITE_MEMBERS_BANNER,
group_id: @group.id } }
= render 'groups/invite_members_modal', group: @group
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
index 25a7f7ba9d7..90f3ac61614 100644
--- a/app/views/layouts/header/_registration_enabled_callout.html.haml
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -4,7 +4,7 @@
title: _('Open registration is enabled on your instance.'),
variant: :warning,
alert_class: 'js-registration-enabled-callout',
- alert_data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path },
+ alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: callouts_path },
close_button_data: { testid: 'close-registration-enabled-callout' } do
.gl-alert-body
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\" class=\"gl-link\">".html_safe, anchorClose: '</a>'.html_safe }
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
index 097475d2928..9fef9864475 100644
--- a/app/views/projects/feature_flags/new.html.haml
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -6,8 +6,8 @@
#js-new-feature-flag{ data: { endpoint: project_feature_flags_path(@project, format: :json),
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json),
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-environments-with-specs'),
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 44336b95e0f..7af825b2819 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -7,4 +7,7 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
-#js-job-vue-app{ data: jobs_data }
+- if @build.is_a? ::Ci::Build
+ #js-job-page{ data: jobs_data }
+- else
+ #js-bridge-page{ data: bridge_data(@build) }
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index cfdbf3410b1..03927cd3bfa 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -22,6 +22,6 @@
"cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project),
connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s,
- user_callouts_path: user_callouts_path,
- user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
+ user_callouts_path: callouts_path,
+ user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s, } }
diff --git a/app/views/root/index.html.haml b/app/views/root/index.html.haml
index 97dd8e133f5..4b1ac213d68 100644
--- a/app/views/root/index.html.haml
+++ b/app/views/root/index.html.haml
@@ -3,8 +3,8 @@
.gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
.js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
- callouts_path: user_callouts_path,
- callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE,
+ callouts_path: callouts_path,
+ callouts_feature_id: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE,
track_label: 'home_page' } }
= render template: 'dashboard/projects/index'
diff --git a/app/views/shared/_flash_user_callout.html.haml b/app/views/shared/_flash_user_callout.html.haml
index d8032ac521d..7b2d59407b4 100644
--- a/app/views/shared/_flash_user_callout.html.haml
+++ b/app/views/shared/_flash_user_callout.html.haml
@@ -1,4 +1,4 @@
-- callout_data = { uid: "callout_feature_#{feature_name}_dismissed", feature_id: feature_name, dismiss_endpoint: user_callouts_path }
+- callout_data = { uid: "callout_feature_#{feature_name}_dismissed", feature_id: feature_name, dismiss_endpoint: callouts_path }
- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
.flash-container.flash-container-page.user-callout{ data: callout_data }
diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
index d4764d1a5d9..e7239661313 100644
--- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
+++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
@@ -1,7 +1,7 @@
= render 'shared/global_alert',
variant: :warning,
alert_class: 'js-recovery-settings-callout',
- alert_data: { feature_id: UserCalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-account-recovery-regular-check-callout' } do
.gl-alert-body
= s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.')
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 3524a1b17ea..8c49977fe82 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -3,24 +3,20 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- %li.nav-item
- = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
- = _('Issues')
- %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
+ = gl_tabs_nav({ class: %w[scrolling-tabs js-milestone-tabs] }) do
+ = gl_tab_link_to '#tab-issues', item_active: true, data: { endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
+ = _('Issues')
+ = gl_tab_counter_badge milestone.issues_visible_to_user(current_user).size
- if milestone.merge_requests_enabled?
- %li.nav-item
- = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do
- = _('Merge requests')
- %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- %li.nav-item
- = link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
- = _('Participants')
- %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
- %li.nav-item
- = link_to '#tab-labels', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'labels') } do
- = _('Labels')
- %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
+ = gl_tab_link_to '#tab-merge-requests', data: { endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do
+ = _('Merge requests')
+ = gl_tab_counter_badge milestone.merge_requests_visible_to_user(current_user).size
+ = gl_tab_link_to '#tab-participants', data: { endpoint: milestone_tab_path(milestone, 'participants') } do
+ = _('Participants')
+ = gl_tab_counter_badge milestone.issue_participants_visible_by_user(current_user).count
+ = gl_tab_link_to '#tab-labels', data: { endpoint: milestone_tab_path(milestone, 'labels') } do
+ = _('Labels')
+ = gl_tab_counter_badge milestone.issue_labels_visible_by_user(current_user).count
.tab-content.milestone-content
.tab-pane.active#tab-issues