diff options
Diffstat (limited to 'app/assets')
17 files changed, 244 insertions, 450 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; |