Commit bdce3a86 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 973f1f888de5
......@@ -2,6 +2,32 @@
.if-canonical-dot-com-gitlab-org-group-master-refs: &if-canonical-dot-com-gitlab-org-group-master-refs
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_COMMIT_REF_NAME == "master"'
# Make sure to update all the similar patterns in other CI config files if you modify these patterns
.code-backstage-qa-patterns: &code-backstage-qa-patterns
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
- "Rakefile"
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
# Backstage changes
- "Dangerfile"
- "danger/**/*"
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
# QA changes
- ".dockerignore"
- "qa/**/*"
pages:
extends:
- .default-tags
......@@ -9,6 +35,7 @@ pages:
- .default-cache
rules:
- <<: *if-canonical-dot-com-gitlab-org-group-master-refs
changes: *code-backstage-qa-patterns
when: on_success
stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
......
......@@ -68,6 +68,7 @@ setup-test-env:
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- junit_rspec.xml
reports:
junit: junit_rspec.xml
......
......@@ -488,3 +488,8 @@ gem 'liquid', '~> 4.0'
gem 'lru_redux'
gem 'erubi', '~> 1.9.0'
# Locked as long as quoted-printable encoding issues are not resolved
# Monkey-patched in `config/initializers/mail_encoding_patch.rb`
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
gem 'mail', '= 2.7.1'
......@@ -1283,6 +1283,7 @@ DEPENDENCIES
lograge (~> 0.5)
loofah (~> 2.2)
lru_redux
mail (= 2.7.1)
mail_room (~> 0.10.0)
marginalia (~> 1.8.0)
memory_profiler (~> 0.9)
......
......@@ -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));
},
......
......@@ -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>
......
<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>
<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>
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);
},
});
};
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 });
},
};
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(),
});
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';
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;
},
};
export default () => ({
projectPath: null,
commitId: null,
blobPath: null,
loading: false,
data: null,
currentDefinition: null,
currentDefinitionPosition: null,
});
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');
}
};
......@@ -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());
}
});
......@@ -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">
......
export const fieldTypes = {
codeBock: 'codeBlock',
link: 'link',
miliseconds: 'miliseconds',
seconds: 'seconds',
text: 'text',
};
......
......@@ -48,7 +48,7 @@ export default () => ({
execution_time: {
value: null,
text: s__('Reports|Execution time'),
type: fieldTypes.miliseconds,
type: fieldTypes.seconds,
},
failure: {
value: null,
......
......@@ -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 }) => {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment