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:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-24 00:09:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-24 00:09:27 +0300
commit17bb9dd270c78fad45851c6cc6ec6e6fdb3d23bf (patch)
treeaa7235893811d97055b3fc750d139a039ae95b0a /app
parentabd2c6b32aabff4654b6be9cb98b59dcd3193fc4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_cloud_connection_form.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue26
-rw-r--r--app/assets/javascripts/ci/runner/constants.js1
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue24
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue24
-rw-r--r--app/assets/javascripts/constants.js2
-rw-r--r--app/assets/javascripts/gl_form.js16
-rw-r--r--app/assets/javascripts/observability/client.js20
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue12
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js8
-rw-r--r--app/assets/javascripts/repository/index.js2
-rw-r--r--app/assets/javascripts/repository/mixins/highlight_mixin.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue356
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue175
-rw-r--r--app/assets/stylesheets/page_bundles/login.scss6
-rw-r--r--app/controllers/groups/runners_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/deployments_controller.rb6
-rw-r--r--app/controllers/projects/runners_controller.rb4
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/models/packages/protection/rule.rb13
-rw-r--r--app/services/packages/npm/create_package_service.rb2
-rw-r--r--app/views/layouts/devise.html.haml5
-rw-r--r--app/views/projects/deployments/show.html.haml4
27 files changed, 273 insertions, 462 deletions
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index 97163c1f55c..1e4ef535e1b 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -59,7 +59,8 @@ export default {
<h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
- <runner-platforms-radio-group v-model="platform" />
+
+ <runner-platforms-radio-group v-model="platform" admin />
<hr aria-hidden="true" />
diff --git a/app/assets/javascripts/ci/runner/components/runner_cloud_connection_form.vue b/app/assets/javascripts/ci/runner/components/runner_cloud_connection_form.vue
new file mode 100644
index 00000000000..c213607670e
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_cloud_connection_form.vue
@@ -0,0 +1,15 @@
+<script>
+import { s__ } from '~/locale';
+
+export default {
+ name: 'RunnerCloudForm',
+ i18n: {
+ title: s__('Runners|Google Cloud connection'),
+ },
+};
+</script>
+<template>
+ <div>
+ <h2 class="gl-font-size-h2">{{ $options.i18n.title }}</h2>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
index a841f66b566..ba50932be4e 100644
--- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
@@ -3,11 +3,13 @@ import DOCKER_LOGO_URL from '@gitlab/svgs/dist/illustrations/third-party-logos/c
import LINUX_LOGO_URL from '@gitlab/svgs/dist/illustrations/third-party-logos/linux.svg?url';
import KUBERNETES_LOGO_URL from '@gitlab/svgs/dist/illustrations/logos/kubernetes.svg?url';
import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
+ GOOGLE_CLOUD_PLATFORM,
DOCKER_HELP_URL,
KUBERNETES_HELP_URL,
} from '../constants';
@@ -21,18 +23,29 @@ export default {
GlIcon,
RunnerPlatformsRadio,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
value: {
type: String,
required: false,
default: null,
},
+ admin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
model: this.value,
};
},
+ computed: {
+ gcpEnabled() {
+ return this.glFeatures.gcpRunner && !this.admin;
+ },
+ },
watch: {
model() {
this.$emit('input', this.model);
@@ -42,7 +55,7 @@ export default {
LINUX_LOGO_URL,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
-
+ GOOGLE_CLOUD_PLATFORM,
DOCKER_HELP_URL,
DOCKER_LOGO_URL,
KUBERNETES_HELP_URL,
@@ -73,6 +86,17 @@ export default {
</div>
</div>
+ <div v-if="gcpEnabled" class="gl-mt-3 gl-mb-6">
+ <label>{{ s__('Runners|Cloud') }}</label>
+
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3">
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <runner-platforms-radio v-model="model" :value="$options.GOOGLE_CLOUD_PLATFORM">
+ Google Cloud
+ </runner-platforms-radio>
+ </div>
+ </div>
+
<div class="gl-mt-3 gl-mb-6">
<label>{{ s__('Runners|Containers') }}</label>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index d04d75b6e75..b275a8f5749 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -220,6 +220,7 @@ export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners';
export const LINUX_PLATFORM = 'linux';
export const MACOS_PLATFORM = 'osx';
export const WINDOWS_PLATFORM = 'windows';
+export const GOOGLE_CLOUD_PLATFORM = 'google';
// About Gitlab Runner Package host
export const RUNNER_PACKAGE_HOST = 'gitlab-runner-downloads.s3.amazonaws.com';
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index c907f9c8982..21058c93d15 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -2,11 +2,17 @@
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import RunnerCloudConnectionForm from '~/ci/runner/components/runner_cloud_connection_form.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
-import { DEFAULT_PLATFORM, GROUP_TYPE, PARAM_KEY_PLATFORM } from '../constants';
+import {
+ DEFAULT_PLATFORM,
+ GOOGLE_CLOUD_PLATFORM,
+ GROUP_TYPE,
+ PARAM_KEY_PLATFORM,
+} from '../constants';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
export default {
@@ -14,8 +20,10 @@ export default {
components: {
RegistrationCompatibilityAlert,
RunnerPlatformsRadioGroup,
+ RunnerCloudConnectionForm,
RunnerCreateForm,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
groupId: {
type: String,
@@ -27,6 +35,14 @@ export default {
platform: DEFAULT_PLATFORM,
};
},
+ computed: {
+ gcpEnabled() {
+ return this.glFeatures.gcpRunner;
+ },
+ showCloudForm() {
+ return this.platform === GOOGLE_CLOUD_PLATFORM && this.gcpEnabled;
+ },
+ },
methods: {
onSaved(runner) {
const params = { [PARAM_KEY_PLATFORM]: this.platform };
@@ -65,11 +81,15 @@ export default {
<h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
+
<runner-platforms-radio-group v-model="platform" />
<hr aria-hidden="true" />
+ <runner-cloud-connection-form v-if="showCloudForm" />
+
<runner-create-form
+ v-else
:runner-type="$options.GROUP_TYPE"
:group-id="groupId"
@saved="onSaved"
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index 241479a8c98..8f3dfbf42ad 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -2,11 +2,17 @@
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import RunnerCloudConnectionForm from '~/ci/runner/components/runner_cloud_connection_form.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
-import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants';
+import {
+ DEFAULT_PLATFORM,
+ PARAM_KEY_PLATFORM,
+ GOOGLE_CLOUD_PLATFORM,
+ PROJECT_TYPE,
+} from '../constants';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
export default {
@@ -14,8 +20,10 @@ export default {
components: {
RegistrationCompatibilityAlert,
RunnerPlatformsRadioGroup,
+ RunnerCloudConnectionForm,
RunnerCreateForm,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
projectId: {
type: String,
@@ -27,6 +35,14 @@ export default {
platform: DEFAULT_PLATFORM,
};
},
+ computed: {
+ gcpEnabled() {
+ return this.glFeatures.gcpRunner;
+ },
+ showCloudForm() {
+ return this.platform === GOOGLE_CLOUD_PLATFORM && this.gcpEnabled;
+ },
+ },
methods: {
onSaved(runner) {
const params = { [PARAM_KEY_PLATFORM]: this.platform };
@@ -65,11 +81,15 @@ export default {
<h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
+
<runner-platforms-radio-group v-model="platform" />
<hr aria-hidden="true" />
+ <runner-cloud-connection-form v-if="showCloudForm" />
+
<runner-create-form
+ v-else
:runner-type="$options.PROJECT_TYPE"
:project-id="projectId"
@saved="onSaved"
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js
index f43a2d5d8ff..631968ff531 100644
--- a/app/assets/javascripts/constants.js
+++ b/app/assets/javascripts/constants.js
@@ -3,3 +3,5 @@ export const getModifierKey = (removeSuffix = false) => {
const winKey = `Ctrl${removeSuffix ? '' : '+'}`;
return window.gl?.client?.isMac ? '⌘' : winKey;
};
+
+export const PRELOAD_THROTTLE_TIMEOUT_MS = 4000;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index f4008fe3cc9..776f27a8583 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -5,6 +5,7 @@ import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_
import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
+import { PRELOAD_THROTTLE_TIMEOUT_MS } from './constants';
export default class GLForm {
/**
@@ -68,6 +69,21 @@ export default class GLForm {
);
this.autoComplete = new GfmAutoComplete(dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
+
+ if (this.preloadMembers && dataSources?.members) {
+ // for now the preload is only implemented for the members
+ // timeout helping to trottle the preloads in the case content_editor
+ // is set as main comment editor and support for rspec tests
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/427437
+
+ requestIdleCallback(() =>
+ setTimeout(
+ () => this.autoComplete?.fetchData($('.js-gfm-input'), '@'),
+ PRELOAD_THROTTLE_TIMEOUT_MS,
+ ),
+ );
+ }
+
this.formDropzone = dropzoneInput(this.form, { parallelUploads: 1 });
if (this.form.is(':not(.js-no-autosize)')) {
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 4fc4ce06528..d3ed168b68e 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -235,6 +235,20 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sort
}
}
+async function fetchTracesAnalytics(tracingAnalyticsUrl, { filters = {} } = {}) {
+ const params = filterObjToQueryParams(filters);
+
+ try {
+ const { data } = await axios.get(tracingAnalyticsUrl, {
+ withCredentials: true,
+ params,
+ });
+ return data.results ?? [];
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
async function fetchServices(servicesUrl) {
try {
const { data } = await axios.get(servicesUrl, {
@@ -339,6 +353,7 @@ export function buildClient(config) {
const {
provisioningUrl,
tracingUrl,
+ tracingAnalyticsUrl,
servicesUrl,
operationsUrl,
metricsUrl,
@@ -353,6 +368,10 @@ export function buildClient(config) {
throw new Error('tracingUrl param must be a string');
}
+ if (typeof tracingAnalyticsUrl !== 'string') {
+ throw new Error('tracingAnalyticsUrl param must be a string');
+ }
+
if (typeof servicesUrl !== 'string') {
throw new Error('servicesUrl param must be a string');
}
@@ -373,6 +392,7 @@ export function buildClient(config) {
enableObservability: () => enableObservability(provisioningUrl),
isObservabilityEnabled: () => isObservabilityEnabled(provisioningUrl),
fetchTraces: (options) => fetchTraces(tracingUrl, options),
+ fetchTracesAnalytics: (options) => fetchTracesAnalytics(tracingAnalyticsUrl, options),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index d42fb10063e..399ea1cc257 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -85,7 +85,7 @@ if (viewBlobEl) {
router,
apolloProvider,
provide: {
- highlightWorker: gon.features.highlightJsWorker ? new HighlightWorker() : null,
+ highlightWorker: new HighlightWorker(),
targetBranch,
originalBranch,
resourceId,
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 8cca70e07a2..8033b5f1225 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -9,7 +9,6 @@ import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
@@ -33,7 +32,7 @@ export default {
CodeIntelligence,
AiGenie: () => import('ee_component/ai/components/ai_genie.vue'),
},
- mixins: [getRefMixin, glFeatureFlagMixin(), highlightMixin],
+ mixins: [getRefMixin, highlightMixin],
inject: {
originalBranch: {
default: '',
@@ -150,14 +149,7 @@ export default {
},
blobViewer() {
const { fileType } = this.viewer;
- return this.shouldLoadLegacyViewer
- ? null
- : loadViewer(
- fileType,
- this.isUsingLfs,
- this.glFeatures.highlightJsWorker,
- this.blobInfo.language,
- );
+ return this.shouldLoadLegacyViewer ? null : loadViewer(fileType, this.isUsingLfs);
},
shouldLoadLegacyViewer() {
return LEGACY_FILE_TYPES.includes(this.blobInfo.fileType) || this.useFallback;
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index 016f7f9fe43..96efbc26a33 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -1,5 +1,3 @@
-import { TEXT_FILE_TYPE } from '../../constants';
-
export const viewers = {
csv: () => import('./csv_viewer.vue'),
download: () => import('./download_viewer.vue'),
@@ -17,13 +15,9 @@ export const viewers = {
geo_json: () => import('./geo_json/geo_json_viewer.vue'),
};
-export const loadViewer = (type, isUsingLfs, hljsWorkerEnabled) => {
+export const loadViewer = (type, isUsingLfs) => {
let viewer = viewers[type];
- if (hljsWorkerEnabled && type === TEXT_FILE_TYPE) {
- viewer = () => import('~/vue_shared/components/source_viewer/source_viewer_new.vue');
- }
-
if (!viewer && isUsingLfs) {
viewer = viewers.lfs;
}
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index afe3f7b1983..ddec4039c73 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -293,7 +293,7 @@ export default function setupVueRepositoryList() {
resourceId,
userId,
explainCodeAvailable: parseBoolean(explainCodeAvailable),
- highlightWorker: gon.features.highlightJsWorker ? new HighlightWorker() : null,
+ highlightWorker: new HighlightWorker(),
},
render(h) {
return h(App);
diff --git a/app/assets/javascripts/repository/mixins/highlight_mixin.js b/app/assets/javascripts/repository/mixins/highlight_mixin.js
index 422a84dff40..1cf182e8f90 100644
--- a/app/assets/javascripts/repository/mixins/highlight_mixin.js
+++ b/app/assets/javascripts/repository/mixins/highlight_mixin.js
@@ -49,7 +49,7 @@ export default {
initHighlightWorker(blob, isUsingLfs) {
const { rawTextBlob, language, fileType, externalStorageUrl, rawPath, simpleViewer } = blob;
- if (!this.glFeatures.highlightJsWorker || simpleViewer?.fileType !== TEXT_FILE_TYPE) return;
+ if (simpleViewer?.fileType !== TEXT_FILE_TYPE) return;
if (this.isUnsupportedLanguage(language)) {
this.handleUnsupportedLanguage(language);
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 4d5d877d43b..1dd001bd4f5 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -1,46 +1,42 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
-import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
-import axios from '~/lib/utils/axios_utils';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
- LEGACY_FALLBACKS,
- CODEOWNERS_FILE_NAME,
- CODEOWNERS_LANGUAGE,
- SVELTE_LANGUAGE,
-} from './constants';
-import Chunk from './components/chunk.vue';
-import { registerPlugins } from './plugins/index';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import LineHighlighter from '~/blob/line_highlighter';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER, CODEOWNERS_FILE_NAME } from './constants';
+import Chunk from './components/chunk_new.vue';
+import Blame from './components/blame_info.vue';
+import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
+import blameDataQuery from './queries/blame_data.query.graphql';
-/*
- * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
- * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible.
- *
- * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback).
- * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes,
- * it does not trigger a repaint on a parent element that wraps all 1000 lines.
- */
export default {
name: 'SourceViewer',
components: {
- GlLoadingIcon,
Chunk,
+ Blame,
CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
+ directives: {
+ SafeHtml,
+ },
mixins: [Tracking.mixin()],
props: {
blob: {
type: Object,
required: true,
},
+ chunks: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showBlame: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
projectPath: {
type: String,
required: true,
@@ -52,249 +48,123 @@ export default {
},
data() {
return {
- languageDefinition: null,
- content: this.blob.rawTextBlob,
- hljs: null,
- firstChunk: null,
- chunks: {},
- isLoading: true,
- lineHighlighter: null,
+ lineHighlighter: new LineHighlighter(),
+ blameData: [],
+ renderedChunks: [],
};
},
computed: {
- isLfsBlob() {
- const { storedExternally, externalStorage, simpleViewer } = this.blob;
-
- return storedExternally && externalStorage === 'lfs' && simpleViewer?.fileType === 'text';
- },
- splitContent() {
- return this.content.split(/\r?\n/);
- },
- language() {
- if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
- // override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
- return SVELTE_LANGUAGE;
- }
- if (this.isCodeownersFile) {
- // override for codeowners files
- return this.$options.codeownersLanguage;
- }
-
- return ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language?.toLowerCase()];
- },
- lineNumbers() {
- return this.splitContent.length;
- },
- unsupportedLanguage() {
- const supportedLanguages = Object.keys(languageLoader);
- const unsupportedLanguage =
- !supportedLanguages.includes(this.language) &&
- !supportedLanguages.includes(this.blob.language?.toLowerCase());
+ blameInfo() {
+ return this.blameData.reduce((result, blame, index) => {
+ if (shouldRender(this.blameData, index)) {
+ result.push({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ });
+ }
- return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage;
- },
- totalChunks() {
- return Object.keys(this.chunks).length;
+ return result;
+ }, []);
},
isCodeownersFile() {
return this.blob.name === CODEOWNERS_FILE_NAME;
},
},
- async created() {
- if (this.isLfsBlob) {
- await axios
- .get(this.blob.externalStorageUrl || this.blob.rawPath)
- .then((result) => {
- this.content = result.data;
- })
- .catch(() => this.$emit('error'));
- }
-
+ watch: {
+ showBlame: {
+ handler(shouldShow) {
+ toggleBlameClasses(this.blameData, shouldShow);
+ this.requestBlameInfo(this.renderedChunks[0]);
+ },
+ immediate: true,
+ },
+ blameData: {
+ handler(blameData) {
+ if (!this.showBlame) return;
+ toggleBlameClasses(blameData, true);
+ },
+ immediate: true,
+ },
+ },
+ created() {
+ this.handleAppear = debounce(this.handleChunkAppear, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
- this.trackEvent(EVENT_LABEL_VIEWER);
-
- if (this.unsupportedLanguage) {
- this.handleUnsupportedLanguage();
- return;
- }
-
- this.generateFirstChunk();
- this.hljs = await this.loadHighlightJS();
-
- if (this.language) {
- this.languageDefinition = await this.loadLanguage();
- }
-
- // Highlight the first chunk as soon as highlight.js is available
- this.highlightChunk(null, true);
-
- window.requestIdleCallback(async () => {
- // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first
- this.generateRemainingChunks();
- this.isLoading = false;
- await this.$nextTick();
- this.selectLine();
- });
+ },
+ mounted() {
+ this.selectLine();
},
methods: {
- trackEvent(label) {
- this.track(EVENT_ACTION, { label, property: this.blob.language });
- },
- handleUnsupportedLanguage() {
- this.trackEvent(EVENT_LABEL_FALLBACK);
- this.$emit('error');
- },
- generateFirstChunk() {
- const lines = this.splitContent.splice(0, LINES_PER_CHUNK);
- this.firstChunk = this.createChunk(lines);
- },
- generateRemainingChunks() {
- const result = {};
- for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) {
- const chunkIndex = Math.floor(i / LINES_PER_CHUNK);
- const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK);
- result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK);
- }
-
- this.chunks = result;
- },
- createChunk(lines, startingFrom = 0) {
- return {
- content: lines.join('\n'),
- startingFrom,
- totalLines: lines.length,
- language: this.language,
- isHighlighted: false,
- };
- },
- highlightChunk(index, isFirstChunk) {
- const chunk = isFirstChunk ? this.firstChunk : this.chunks[index];
-
- if (chunk.isHighlighted) {
- return;
- }
-
- const { highlightedContent, language } = this.highlight(chunk.content, this.language);
-
- Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true });
-
- this.selectLine();
-
- this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path));
- },
- highlight(content, language) {
- let detectedLanguage = language;
- let highlightedContent;
- if (this.hljs) {
- registerPlugins(this.hljs, this.blob.fileType, this.content);
- if (!detectedLanguage) {
- const hljsHighlightAuto = this.hljs.highlightAuto(content);
- highlightedContent = hljsHighlightAuto.value;
- detectedLanguage = hljsHighlightAuto.language;
- } else if (this.languageDefinition) {
- highlightedContent = this.hljs.highlight(content, { language: this.language }).value;
+ async handleChunkAppear(chunkIndex, handleOverlappingChunk = true) {
+ if (!this.renderedChunks.includes(chunkIndex)) {
+ this.renderedChunks.push(chunkIndex);
+ await this.requestBlameInfo(chunkIndex);
+
+ if (chunkIndex > 0 && handleOverlappingChunk) {
+ // request the blame information for overlapping chunk incase it is visible in the DOM
+ this.handleChunkAppear(chunkIndex - 1, false);
}
}
-
- return { highlightedContent, language: detectedLanguage };
},
- loadHighlightJS() {
- // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint)
- return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core');
- },
- async loadSubLanguages(languageDefinition) {
- if (!languageDefinition?.contains) return;
-
- // generate list of languages to load
- const languages = new Set(
- languageDefinition.contains
- .filter((component) => Boolean(component.subLanguage))
- .map((component) => component.subLanguage),
- );
-
- if (languageDefinition.subLanguage) {
- languages.add(languageDefinition.subLanguage);
- }
-
- // load all sub-languages at once
- await Promise.all(
- [...languages].map(async (subLanguage) => {
- const subLanguageDefinition = await languageLoader[subLanguage]();
- this.hljs.registerLanguage(subLanguage, subLanguageDefinition.default);
- }),
- );
- },
- async loadLanguage() {
- let languageDefinition;
-
- try {
- languageDefinition = await languageLoader[this.language]();
- this.hljs.registerLanguage(this.language, languageDefinition.default);
-
- await this.loadSubLanguages(this.hljs.getLanguage(this.language));
- } catch (message) {
- this.$emit('error', message);
- }
-
- return languageDefinition;
+ async requestBlameInfo(chunkIndex) {
+ const chunk = this.chunks[chunkIndex];
+ if (!this.showBlame || !chunk) return;
+
+ const { data } = await this.$apollo.query({
+ query: blameDataQuery,
+ variables: {
+ ref: this.currentRef,
+ fullPath: this.projectPath,
+ filePath: this.blob.path,
+ fromLine: chunk.startingFrom + 1,
+ toLine: chunk.startingFrom + chunk.totalLines,
+ },
+ });
+
+ const blob = data?.project?.repository?.blobs?.nodes[0];
+ const blameGroups = blob?.blame?.groups;
+ const isDuplicate = this.blameData.includes(blameGroups[0]);
+ if (blameGroups && !isDuplicate) this.blameData.push(...blameGroups);
},
async selectLine() {
- if (!this.lineHighlighter) {
- this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
- }
await this.$nextTick();
- const scrollEnabled = false;
- this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ this.lineHighlighter.highlightHash(this.$route.hash);
},
},
userColorScheme: window.gon.user_color_scheme,
- currentlySelectedLine: null,
- codeownersLanguage: CODEOWNERS_LANGUAGE,
};
</script>
-<template>
- <div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
- :class="$options.userColorScheme"
- data-type="simple"
- :data-path="blob.path"
- data-testid="blob-viewer-file-content"
- >
- <codeowners-validation
- v-if="isCodeownersFile"
- class="gl-text-black-normal"
- :current-ref="currentRef"
- :project-path="projectPath"
- :file-path="blob.path"
- />
- <chunk
- v-if="firstChunk"
- :lines="firstChunk.lines"
- :total-lines="firstChunk.totalLines"
- :content="firstChunk.content"
- :starting-from="firstChunk.startingFrom"
- :is-highlighted="firstChunk.isHighlighted"
- is-first-chunk
- :language="firstChunk.language"
- :blame-path="blob.blamePath"
- />
- <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
- <template v-else>
+<template>
+ <div class="gl-display-flex">
+ <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
+
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full blob-viewer"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ data-testid="blob-viewer-file-content"
+ >
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
- v-for="(chunk, key, index) in chunks"
- :key="key"
- :lines="chunk.lines"
- :content="chunk.content"
+ v-for="(chunk, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
- :is-highlighted="chunk.isHighlighted"
- :chunk-index="index"
- :language="chunk.language"
:blame-path="blob.blamePath"
- :total-chunks="totalChunks"
- @appear="highlightChunk"
+ @appear="() => handleAppear(index)"
/>
- </template>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
deleted file mode 100644
index e62f38d9ca3..00000000000
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ /dev/null
@@ -1,175 +0,0 @@
-<script>
-import { debounce } from 'lodash';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import Tracking from '~/tracking';
-import addBlobLinksTracking from '~/blob/blob_links_tracking';
-import LineHighlighter from '~/blob/line_highlighter';
-import { EVENT_ACTION, EVENT_LABEL_VIEWER, CODEOWNERS_FILE_NAME } from './constants';
-import Chunk from './components/chunk_new.vue';
-import Blame from './components/blame_info.vue';
-import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
-import blameDataQuery from './queries/blame_data.query.graphql';
-
-/*
- * Note, this is a new experimental version of the SourceViewer, it is not ready for production use.
- * See the following issue for more details: https://gitlab.com/gitlab-org/gitlab/-/issues/391586
- */
-
-export default {
- name: 'SourceViewerNew',
- components: {
- Chunk,
- Blame,
- CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
- },
- directives: {
- SafeHtml,
- },
- mixins: [Tracking.mixin()],
- props: {
- blob: {
- type: Object,
- required: true,
- },
- chunks: {
- type: Array,
- required: false,
- default: () => [],
- },
- showBlame: {
- type: Boolean,
- required: false,
- default: false,
- },
- projectPath: {
- type: String,
- required: true,
- },
- currentRef: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- lineHighlighter: new LineHighlighter(),
- blameData: [],
- renderedChunks: [],
- };
- },
- computed: {
- blameInfo() {
- return this.blameData.reduce((result, blame, index) => {
- if (shouldRender(this.blameData, index)) {
- result.push({
- ...blame,
- blameOffset: calculateBlameOffset(blame.lineno, index),
- });
- }
-
- return result;
- }, []);
- },
- isCodeownersFile() {
- return this.blob.name === CODEOWNERS_FILE_NAME;
- },
- },
- watch: {
- showBlame: {
- handler(shouldShow) {
- toggleBlameClasses(this.blameData, shouldShow);
- this.requestBlameInfo(this.renderedChunks[0]);
- },
- immediate: true,
- },
- blameData: {
- handler(blameData) {
- if (!this.showBlame) return;
- toggleBlameClasses(blameData, true);
- },
- immediate: true,
- },
- },
- created() {
- this.handleAppear = debounce(this.handleChunkAppear, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
- addBlobLinksTracking();
- },
- mounted() {
- this.selectLine();
- },
- methods: {
- async handleChunkAppear(chunkIndex, handleOverlappingChunk = true) {
- if (!this.renderedChunks.includes(chunkIndex)) {
- this.renderedChunks.push(chunkIndex);
- await this.requestBlameInfo(chunkIndex);
-
- if (chunkIndex > 0 && handleOverlappingChunk) {
- // request the blame information for overlapping chunk incase it is visible in the DOM
- this.handleChunkAppear(chunkIndex - 1, false);
- }
- }
- },
- async requestBlameInfo(chunkIndex) {
- const chunk = this.chunks[chunkIndex];
- if (!this.showBlame || !chunk) return;
-
- const { data } = await this.$apollo.query({
- query: blameDataQuery,
- variables: {
- ref: this.currentRef,
- fullPath: this.projectPath,
- filePath: this.blob.path,
- fromLine: chunk.startingFrom + 1,
- toLine: chunk.startingFrom + chunk.totalLines,
- },
- });
-
- const blob = data?.project?.repository?.blobs?.nodes[0];
- const blameGroups = blob?.blame?.groups;
- const isDuplicate = this.blameData.includes(blameGroups[0]);
- if (blameGroups && !isDuplicate) this.blameData.push(...blameGroups);
- },
- async selectLine() {
- await this.$nextTick();
- this.lineHighlighter.highlightHash(this.$route.hash);
- },
- },
- userColorScheme: window.gon.user_color_scheme,
-};
-</script>
-
-<template>
- <div class="gl-display-flex">
- <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
-
- <div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full blob-viewer"
- :class="$options.userColorScheme"
- data-type="simple"
- :data-path="blob.path"
- data-testid="blob-viewer-file-content"
- >
- <codeowners-validation
- v-if="isCodeownersFile"
- class="gl-text-black-normal"
- :current-ref="currentRef"
- :project-path="projectPath"
- :file-path="blob.path"
- />
- <chunk
- v-for="(chunk, index) in chunks"
- :key="index"
- :chunk-index="index"
- :is-highlighted="Boolean(chunk.isHighlighted)"
- :raw-content="chunk.rawContent"
- :highlighted-content="chunk.highlightedContent"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :blame-path="blob.blamePath"
- @appear="() => handleAppear(index)"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index f46d80e2525..6444df66849 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -6,12 +6,6 @@
max-width: 960px;
}
- .flash-container {
- margin-bottom: $gl-padding;
- position: relative;
- top: 8px;
- }
-
.borderless {
.login-box {
box-shadow: none;
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 3600a0fbed5..cb6f837b8e3 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -6,6 +6,10 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
+ before_action do
+ push_frontend_feature_flag(:gcp_runner, @project, type: :wip)
+ end
+
feature_category :runner
urgency :low
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 558aac7b1ef..b0eabe92f39 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -49,7 +49,6 @@ class Projects::BlobController < Projects::ApplicationController
urgency :low, [:create, :show, :edit, :update, :diff]
before_action do
- push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:encoding_logs_tree)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index bebade1b21b..07aeb49279d 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -16,6 +16,12 @@ class Projects::DeploymentsController < Projects::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def show
+ return render_404 unless Feature.enabled?(:deployment_details_page, project, type: :wip)
+
+ @deployment = environment.deployments.find_by_iid!(params[:id])
+ end
+
def metrics
return render_404 unless deployment_metrics.has_metrics?
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index db19ca23e9f..01a2d7f04dc 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,6 +5,10 @@ class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_create_runner!, only: [:new, :register]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]
+ before_action do
+ push_frontend_feature_flag(:gcp_runner, @project, type: :wip)
+ end
+
feature_category :runner
urgency :low
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index e98a5fc07d3..9b9dbc507e1 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -19,7 +19,6 @@ class Projects::TreeController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
- push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:encoding_logs_tree)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 23c5e2ad28f..679be9323d4 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -38,7 +38,6 @@ class ProjectsController < Projects::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
- push_frontend_feature_flag(:highlight_js_worker, @project)
push_frontend_feature_flag(:remove_monitor_metrics, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:issue_email_participants, @project)
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index f13bcc6e32e..ff45db40cad 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -23,14 +23,17 @@ module Packages
before_save :set_package_name_pattern_ilike_query, if: :package_name_pattern_changed?
- scope :for_package_name, ->(package_name) {
+ scope :for_package_name, ->(package_name) do
return none if package_name.blank?
- where(':package_name ILIKE package_name_pattern_ilike_query', package_name: package_name)
- }
+ where(
+ ":package_name ILIKE #{::Gitlab::SQL::Glob.to_like('package_name_pattern')}",
+ package_name: package_name
+ )
+ end
- def self.push_protected_from?(access_level:, package_name:, package_type:)
- return true if [access_level, package_name, package_type].any?(&:blank?)
+ def self.for_push_exists?(access_level:, package_name:, package_type:)
+ return false if [access_level, package_name, package_type].any?(&:blank?)
where(package_type: package_type, push_protected_up_to_access_level: access_level..)
.for_package_name(package_name)
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index a27f059036c..b1970053745 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -72,7 +72,7 @@ module Packages
return false if Feature.disabled?(:packages_protected_packages, project)
user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
- project.package_protection_rules.push_protected_from?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm)
+ project.package_protection_rules.for_push_exists?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm)
end
def name
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 2905ba924ca..0ae2e5337f5 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -12,7 +12,7 @@
.content
= render "layouts/flash"
- if custom_text.present?
- .row
+ .row.gl-mt-5.gl-row-gap-5
.col-md.order-12.sm-bg-gray
.col-sm-12
%h1.mb-3.gl-font-size-h2
@@ -24,12 +24,11 @@
= brand_image
= yield
- else
- .mt-3
+ .gl-my-5
.col-sm-12.gl-text-center
= brand_image
%h1.mb-3.gl-font-size-h2
= brand_title
- .mb-3
.gl-w-full.gl-sm-w-half.gl-ml-auto.gl-mr-auto.bar
= yield
diff --git a/app/views/projects/deployments/show.html.haml b/app/views/projects/deployments/show.html.haml
new file mode 100644
index 00000000000..b0ea762b000
--- /dev/null
+++ b/app/views/projects/deployments/show.html.haml
@@ -0,0 +1,4 @@
+- add_to_breadcrumbs _("Environments"), project_environments_path(@project)
+- add_to_breadcrumbs @environment.name, project_environment_path(@project, @environment)
+- breadcrumb_title _("Deployment #%{iid}") % { iid: @deployment.iid }
+- page_title _("Deployment #%{iid}") % { iid: @deployment.iid }