diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
commit | 5564275a0b378298dc6281599cbfe71a937109ff (patch) | |
tree | a468e1e60046356410219c35c23a8a428c5e2c5e /app/assets/javascripts | |
parent | d87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff) | |
download | gitlab-ce-5564275a0b378298dc6281599cbfe71a937109ff.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
16 files changed, 296 insertions, 6 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 6de9ab9efb3..76f3020c5c2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -45,6 +45,7 @@ const Api = { mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', adminStatisticsPath: '/api/:version/application/statistics', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', + lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -457,6 +458,14 @@ const Api = { return axios.get(url); }, + lsifData(projectPath, commitId, path) { + const url = Api.buildUrl(this.lsifPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':commit_id', commitId); + + return axios.get(url, { params: { path } }); + }, + buildUrl(url) { return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); }, diff --git a/app/assets/javascripts/boards/components/issue_count.vue b/app/assets/javascripts/boards/components/issue_count.vue index c50a3c1c0d3..d55f7151d7e 100644 --- a/app/assets/javascripts/boards/components/issue_count.vue +++ b/app/assets/javascripts/boards/components/issue_count.vue @@ -25,7 +25,7 @@ export default { </script> <template> - <div class="issue-count"> + <div class="issue-count text-nowrap"> <span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }"> {{ issuesSize }} </span> diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue new file mode 100644 index 00000000000..0e5f1f0485d --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/app.vue @@ -0,0 +1,43 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import Popover from './popover.vue'; + +export default { + components: { + Popover, + }, + computed: { + ...mapState(['currentDefinition', 'currentDefinitionPosition']), + }, + mounted() { + this.blobViewer = document.querySelector('.blob-viewer'); + + this.addGlobalEventListeners(); + this.fetchData(); + }, + beforeDestroy() { + this.removeGlobalEventListeners(); + }, + methods: { + ...mapActions(['fetchData', 'showDefinition']), + addGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.addEventListener('click', this.showDefinition); + } + }, + removeGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.removeEventListener('click', this.showDefinition); + } + }, + }, +}; +</script> + +<template> + <popover + v-if="currentDefinition" + :position="currentDefinitionPosition" + :data="currentDefinition" + /> +</template> diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue new file mode 100644 index 00000000000..d5bbe430fcd --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -0,0 +1,76 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + props: { + position: { + type: Object, + required: true, + }, + data: { + type: Object, + required: true, + }, + }, + data() { + return { + offsetLeft: 0, + }; + }, + computed: { + positionStyles() { + return { + left: `${this.position.x - this.offsetLeft}px`, + top: `${this.position.y + this.position.height}px`, + }; + }, + }, + watch: { + position: { + handler() { + this.$nextTick(() => this.updateOffsetLeft()); + }, + deep: true, + immediate: true, + }, + }, + methods: { + updateOffsetLeft() { + this.offsetLeft = Math.max( + 0, + this.$el.offsetLeft + this.$el.offsetWidth - window.innerWidth + 20, + ); + }, + }, + colorScheme: gon?.user_color_scheme, +}; +</script> + +<template> + <div + :style="positionStyles" + class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" + > + <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> + <div v-for="(hover, index) in data.hover" :key="index" class="border-bottom"> + <pre + v-if="hover.language" + ref="code-output" + :class="$options.colorScheme" + class="border-0 bg-transparent m-0 code highlight" + v-html="hover.value" + ></pre> + <p v-else ref="doc-output" class="p-3 m-0"> + {{ hover.value }} + </p> + </div> + <div v-if="data.definition_url" class="popover-body"> + <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> + {{ __('Go to definition') }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js new file mode 100644 index 00000000000..2222c986dfe --- /dev/null +++ b/app/assets/javascripts/code_navigation/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import store from './store'; +import App from './components/app.vue'; + +Vue.use(Vuex); + +export default () => { + const el = document.getElementById('js-code-navigation'); + + store.dispatch('setInitialData', el.dataset); + + return new Vue({ + el, + store, + render(h) { + return h(App); + }, + }); +}; diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js new file mode 100644 index 00000000000..10483abfb23 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -0,0 +1,62 @@ +import api from '~/api'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import * as types from './mutation_types'; +import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; + +export default { + setInitialData({ commit }, data) { + commit(types.SET_INITIAL_DATA, data); + }, + requestDataError({ commit }) { + commit(types.REQUEST_DATA_ERROR); + createFlash(__('An error occurred loading code navigation')); + }, + fetchData({ commit, dispatch, state }) { + commit(types.REQUEST_DATA); + + api + .lsifData(state.projectPath, state.commitId, state.path) + .then(({ data }) => { + const normalizedData = data.reduce((acc, d) => { + if (d.hover) { + acc[`${d.start_line}:${d.start_char}`] = d; + addInteractionClass(d); + } + return acc; + }, {}); + + commit(types.REQUEST_DATA_SUCCESS, normalizedData); + }) + .catch(() => dispatch('requestDataError')); + }, + showDefinition({ commit, state }, { target: el }) { + let definition; + let position; + + if (!state.data) return; + + const isCurrentElementPopoverOpen = el.classList.contains('hll'); + + if (getCurrentHoverElement()) { + getCurrentHoverElement().classList.remove('hll'); + } + + if (el.classList.contains('js-code-navigation') && !isCurrentElementPopoverOpen) { + const { lineIndex, charIndex } = el.dataset; + + position = { + x: el.offsetLeft, + y: el.offsetTop, + height: el.offsetHeight, + }; + definition = state.data[`${lineIndex}:${charIndex}`]; + + el.classList.add('hll'); + + setCurrentHoverElement(el); + } + + commit(types.SET_CURRENT_DEFINITION, { definition, position }); + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js new file mode 100644 index 00000000000..fe48f3ac7f5 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/index.js @@ -0,0 +1,10 @@ +import Vuex from 'vuex'; +import createState from './state'; +import actions from './actions'; +import mutations from './mutations'; + +export default new Vuex.Store({ + actions, + mutations, + state: createState(), +}); diff --git a/app/assets/javascripts/code_navigation/store/mutation_types.js b/app/assets/javascripts/code_navigation/store/mutation_types.js new file mode 100644 index 00000000000..29a2897a6fd --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; +export const REQUEST_DATA = 'REQUEST_DATA'; +export const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS'; +export const REQUEST_DATA_ERROR = 'REQUEST_DATA_ERROR'; +export const SET_CURRENT_DEFINITION = 'SET_CURRENT_DEFINITION'; diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js new file mode 100644 index 00000000000..bb833a5adbc --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutations.js @@ -0,0 +1,23 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { + state.projectPath = projectPath; + state.commitId = commitId; + state.blobPath = blobPath; + }, + [types.REQUEST_DATA](state) { + state.loading = true; + }, + [types.REQUEST_DATA_SUCCESS](state, data) { + state.loading = false; + state.data = data; + }, + [types.REQUEST_DATA_ERROR](state) { + state.loading = false; + }, + [types.SET_CURRENT_DEFINITION](state, { definition, position }) { + state.currentDefinition = definition; + state.currentDefinitionPosition = position; + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/state.js b/app/assets/javascripts/code_navigation/store/state.js new file mode 100644 index 00000000000..a7b3b289db4 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/state.js @@ -0,0 +1,9 @@ +export default () => ({ + projectPath: null, + commitId: null, + blobPath: null, + loading: false, + data: null, + currentDefinition: null, + currentDefinitionPosition: null, +}); diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js new file mode 100644 index 00000000000..2dee0de6501 --- /dev/null +++ b/app/assets/javascripts/code_navigation/utils/index.js @@ -0,0 +1,20 @@ +export const cachedData = new Map(); + +export const getCurrentHoverElement = () => cachedData.get('current'); +export const setCurrentHoverElement = el => cachedData.set('current', el); + +export const addInteractionClass = d => { + let charCount = 0; + const line = document.getElementById(`LC${d.start_line + 1}`); + const el = [...line.childNodes].find(({ textContent }) => { + if (charCount === d.start_char) return true; + charCount += textContent.length; + return false; + }); + + if (el) { + el.setAttribute('data-char-index', d.start_char); + el.setAttribute('data-line-index', d.start_line); + el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation'); + } +}; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index aee67899ca2..caf9a8c0b64 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -30,4 +30,9 @@ document.addEventListener('DOMContentLoaded', () => { } GpgBadges.fetch(); + + if (gon.features?.codeNavigation) { + // eslint-disable-next-line promise/catch-or-return + import('~/code_navigation').then(m => m.default()); + } }); diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index 40ce200befb..78c355ecb76 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -46,8 +46,8 @@ export default { </a> </template> - <template v-else-if="field.type === $options.fieldTypes.miliseconds">{{ - sprintf(__('%{value} ms'), { value: field.value }) + <template v-else-if="field.type === $options.fieldTypes.seconds">{{ + sprintf(__('%{value} s'), { value: field.value }) }}</template> <template v-else-if="field.type === $options.fieldTypes.text"> diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 66ac1af062b..1845b51e6b2 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -1,7 +1,7 @@ export const fieldTypes = { codeBock: 'codeBlock', link: 'link', - miliseconds: 'miliseconds', + seconds: 'seconds', text: 'text', }; diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 25f9f70d095..d0b2d0a37f5 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -48,7 +48,7 @@ export default () => ({ execution_time: { value: null, text: s__('Reports|Execution time'), - type: fieldTypes.miliseconds, + type: fieldTypes.seconds, }, failure: { value: null, diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 5b9e3817f3a..67e5f175039 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -54,11 +54,17 @@ const populateUserInfo = user => { ); }; +const initializedPopovers = new Map(); + export default (elements = document.querySelectorAll('.js-user-link')) => { const userLinks = Array.from(elements); + const UserPopoverComponent = Vue.extend(UserPopover); return userLinks.map(el => { - const UserPopoverComponent = Vue.extend(UserPopover); + if (initializedPopovers.has(el)) { + return initializedPopovers.get(el); + } + const user = { location: null, bio: null, @@ -73,6 +79,8 @@ export default (elements = document.querySelectorAll('.js-user-link')) => { }, }); + initializedPopovers.set(el, renderedPopover); + renderedPopover.$mount(); el.addEventListener('mouseenter', ({ target }) => { |