Commit e3592b54 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 4310-security-reports-step-1

* master: (81 commits)
  Fix undefined method `log_transfer_error'
  Resolve the conflict in services_helper.rb
  move render_gfm into behaviors directory
  move render_gfm into behaviors directory
  fix tests for dahboard.vue
  fix tests for dahboard.vue
  Resolve conflict in lib/api/search.rb
  Resolve conflict in vue_merge_request_widget/dependencies.js
  Resolve Gemfile conflicts
  Resolve conflict in prometheus.md
  add no_data illustration to monitoring empty state
  add no_data illustration to monitoring empty state
  Enable controls when overridden
  Add documentation for custom metrics
  Add Cluster Monitoring Documentation
  Add documentation for external authorization
  Improve JIRA event descriptions
  Improve JIRA event descriptions
  Specify timeout for external authorization
  Add external authorization timeout to settings
  ...
parents 80d647df 3551d6a3
...@@ -22,8 +22,8 @@ gem 'faraday', '~> 0.12' ...@@ -22,8 +22,8 @@ gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.2.0' gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.4.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9' gem 'omniauth-azure-oauth2', '~> 0.0.9'
...@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2' ...@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2' gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10.0' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
...@@ -116,7 +116,7 @@ gem 'fog-rackspace', '~> 0.1.1' ...@@ -116,7 +116,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0' gem 'fog-aliyun', '~> 0.2.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.19' gem 'google-api-client', '~> 0.19.8'
# for aws storage # for aws storage
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
...@@ -245,9 +245,6 @@ gem 'mousetrap-rails', '~> 1.4.6' ...@@ -245,9 +245,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding # Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
# Faster JSON
gem 'oj', '~> 2.17.4'
# Faster blank # Faster blank
gem 'fast_blank' gem 'fast_blank'
...@@ -291,7 +288,6 @@ gem 'batch-loader', '~> 1.2.1' ...@@ -291,7 +288,6 @@ gem 'batch-loader', '~> 1.2.1'
# Perf bar # Perf bar
gem 'peek', '~> 1.0.1' gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2' gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0' gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres gem 'peek-pg', '~> 1.3.0', group: :postgres
......
...@@ -47,6 +47,7 @@ GEM ...@@ -47,6 +47,7 @@ GEM
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
addressable (2.5.2) addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.4) arel (6.0.4)
...@@ -94,7 +95,7 @@ GEM ...@@ -94,7 +95,7 @@ GEM
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubis (>= 2.6.6)
rack (>= 0.9.0) rack (>= 0.9.0)
bindata (2.4.1) bindata (2.4.3)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blankslate (2.1.2.4) blankslate (2.1.2.4)
...@@ -184,10 +185,10 @@ GEM ...@@ -184,10 +185,10 @@ GEM
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6) doorkeeper (4.3.1)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.2.0) doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.0) doorkeeper (~> 4.3)
json-jwt (~> 1.6) json-jwt (~> 1.6)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
rails (> 3.1) rails (> 3.1)
...@@ -457,10 +458,10 @@ GEM ...@@ -457,10 +458,10 @@ GEM
jmespath (1.3.1) jmespath (1.3.1)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
json (1.8.6) json (1.8.6)
json-jwt (1.7.2) json-jwt (1.9.2)
activesupport activesupport
aes_key_wrap
bindata bindata
multi_json (>= 1.3)
securecompare securecompare
url_safe_base64 url_safe_base64
json-schema (2.8.0) json-schema (2.8.0)
...@@ -553,7 +554,6 @@ GEM ...@@ -553,7 +554,6 @@ GEM
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.8.0) octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.5)
omniauth (1.4.3) omniauth (1.4.3)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
...@@ -623,8 +623,6 @@ GEM ...@@ -623,8 +623,6 @@ GEM
railties (>= 4.0.0) railties (>= 4.0.0)
peek-gc (0.0.2) peek-gc (0.0.2)
peek peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0) peek-mysql2 (1.1.0)
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
...@@ -707,8 +705,8 @@ GEM ...@@ -707,8 +705,8 @@ GEM
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.8) rails-dom-testing (1.0.9)
activesupport (>= 4.2.0.beta, < 5.0) activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
...@@ -1061,8 +1059,8 @@ DEPENDENCIES ...@@ -1061,8 +1059,8 @@ DEPENDENCIES
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.2.0) doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
elasticsearch-api (= 5.0.3) elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9) elasticsearch-model (~> 0.1.9)
...@@ -1103,7 +1101,7 @@ DEPENDENCIES ...@@ -1103,7 +1101,7 @@ DEPENDENCIES
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.19) google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1) google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
...@@ -1144,7 +1142,6 @@ DEPENDENCIES ...@@ -1144,7 +1142,6 @@ DEPENDENCIES
nokogiri (~> 1.8.2) nokogiri (~> 1.8.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.8) octokit (~> 4.8)
oj (~> 2.17.4)
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1) omniauth-authentiq (~> 0.3.1)
...@@ -1156,14 +1153,13 @@ DEPENDENCIES ...@@ -1156,14 +1153,13 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.5.2) omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10.0) omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0) peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
......
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm'; import './markdown/render_gfm';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard'; import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior'; import './details_behavior';
import installGlEmojiElement from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils'; import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
import { placeholderImage } from '../lazy_loader'; import { placeholderImage } from '~/lazy_loader';
const gfmRules = { const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......
import $ from 'jquery'; import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math'; import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown // Render Gitlab flavoured Markdown
// //
......
import $ from 'jquery'; import $ from 'jquery';
import { __ } from './locale'; import { __ } from '~/locale';
import flash from './flash'; import flash from '~/flash';
// Renders math using KaTeX in any element with the // Renders math using KaTeX in any element with the
// `js-render-math` class // `js-render-math` class
......
import flash from '~/flash';
// Renders diagrams and flowcharts from text using Mermaid in any element with the // Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class. // `js-render-mermaid` class.
// //
...@@ -12,8 +14,6 @@ ...@@ -12,8 +14,6 @@
// </pre> // </pre>
// //
import Flash from './flash';
export default function renderMermaid($els) { export default function renderMermaid($els) {
if (!$els.length) return; if (!$els.length) return;
...@@ -52,6 +52,6 @@ export default function renderMermaid($els) { ...@@ -52,6 +52,6 @@ export default function renderMermaid($els) {
}); });
}); });
}).catch((err) => { }).catch((err) => {
Flash(`Can't load mermaid module: ${err}`); flash(`Can't load mermaid module: ${err}`);
}); });
} }
...@@ -53,8 +53,12 @@ function initPageShortcuts(page) { ...@@ -53,8 +53,12 @@ function initPageShortcuts(page) {
function initGFMInput() { function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const gfm = new GfmAutoComplete(
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
);
const enableGFM = convertPermissionToBoolean(
el.dataset.supportsAutocomplete,
);
gfm.setup($(el), { gfm.setup($(el), {
emojis: true, emojis: true,
members: enableGFM, members: enableGFM,
...@@ -67,9 +71,9 @@ function initGFMInput() { ...@@ -67,9 +71,9 @@ function initGFMInput() {
} }
function initPerformanceBar() { function initPerformanceBar() {
if (document.querySelector('#peek')) { if (document.querySelector('#js-peek')) {
import('./performance_bar') import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module')); .catch(() => Flash('Error loading performance bar module'));
} }
} }
......
...@@ -32,7 +32,6 @@ import LazyLoader from './lazy_loader'; ...@@ -32,7 +32,6 @@ import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo'; import initLogoAnimation from './logo';
import './milestone_select'; import './milestone_select';
import './projects_dropdown'; import './projects_dropdown';
import './render_gfm';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
// EE-only scripts // EE-only scripts
......
...@@ -73,6 +73,10 @@ ...@@ -73,6 +73,10 @@
type: String, type: String,
required: true, required: true,
}, },
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: { emptyUnableToConnectSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -188,6 +192,7 @@ ...@@ -188,6 +192,7 @@
:clusters-path="clustersPath" :clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
/> />
</template> </template>
...@@ -27,6 +27,10 @@ ...@@ -27,6 +27,10 @@
type: String, type: String,
required: true, required: true,
}, },
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: { emptyUnableToConnectSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -54,7 +58,7 @@ ...@@ -54,7 +58,7 @@
buttonPath: this.documentationPath, buttonPath: this.documentationPath,
}, },
noData: { noData: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyNoDataSvgPath,
title: 'No data found', title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently description: `You are connected to the Prometheus server, but there is currently
no data to display.`, no data to display.`,
......
...@@ -105,6 +105,9 @@ export default class Notes { ...@@ -105,6 +105,9 @@ export default class Notes {
this.basePollingInterval = 15000; this.basePollingInterval = 15000;
this.maxPollingSteps = 4; this.maxPollingSteps = 4;
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
this.cleanBinding(); this.cleanBinding();
this.addBinding(); this.addBinding();
this.setPollingInterval(); this.setPollingInterval();
...@@ -138,10 +141,6 @@ export default class Notes { ...@@ -138,10 +141,6 @@ export default class Notes {
} }
addBinding() { addBinding() {
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
// Edit note link // Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this)); this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit); this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
...@@ -226,14 +225,9 @@ export default class Notes { ...@@ -226,14 +225,9 @@ export default class Notes {
$(window).on('hashchange', this.onHashChange); $(window).on('hashchange', this.onHashChange);
this.boundGetContent = this.getContent.bind(this); this.boundGetContent = this.getContent.bind(this);
document.addEventListener('refreshLegacyNotes', this.boundGetContent); document.addEventListener('refreshLegacyNotes', this.boundGetContent);
this.eventsBound = true;
} }
cleanBinding() { cleanBinding() {
if (!this.eventsBound) {
return;
}
this.$wrapperEl.off('click', '.js-note-edit'); this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel'); this.$wrapperEl.off('click', '.note-edit-cancel');
this.$wrapperEl.off('click', '.js-note-delete'); this.$wrapperEl.off('click', '.js-note-delete');
......
import $ from 'jquery';
import 'vendor/peek';
import 'vendor/peek.performance_bar';
import { getParameterValues } from './lib/utils/url_utility';
export default class PerformanceBar {
constructor(opts) {
if (!PerformanceBar.singleton) {
this.init(opts);
PerformanceBar.singleton = this;
}
return PerformanceBar.singleton;
}
init(opts) {
const $container = $(opts.container);
this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
this.$lineProfileModal = $('#modal-peek-line-profile');
this.initEventListeners();
this.showModalOnLoad();
}
initEventListeners() {
this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
$(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
}
showModalOnLoad() {
// When a lineprofiler query-string param is present, we show the line
// profiler modal upon page load
if (/lineprofiler/.test(window.location.search)) {
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
handleLineProfileLink(e) {
const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href);
if (shouldToggleModal) {
e.preventDefault();
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
static toggleModal($modal) {
if ($modal.length) {
$modal.modal('toggle');
}
}
static toggleLineProfileFile(e) {
$(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle();
}
}
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
components: {
GlModal,
},
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
header: {
type: String,
required: true,
},
details: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<button
:data-target="`#modal-peek-${metric}-details`"
class="btn-blank btn-link bold"
type="button"
data-toggle="modal"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
</button>
<gl-modal
v-if="currentRequest.details"
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
class="performance-bar-modal"
>
<table class="table">
<tr
v-for="(item, index) in currentRequest.details[metric][details]"
:key="index"
>
<td><strong>{{ item.duration }}ms</strong></td>
<td
v-for="key in keys"
:key="key"
>
{{ item[key] }}
</td>
</tr>
</table>
<div slot="footer">
</div>
</gl-modal>
{{ metric }}
</div>
</template>
<script>
import $ from 'jquery';
import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
export default {
components: {
detailedMetric,
requestSelector,
simpleMetric,
upstreamPerformanceBar,
},
props: {
store: {
type: Object,
required: true,
},
env: {
type: String,
required: true,
},
requestId: {
type: String,
required: true,
},
peekUrl: {
type: String,
required: true,
},
profileUrl: {
type: String,
required: true,
},
},
detailedMetrics: [
{ metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] },
{
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
},
],
simpleMetrics: ['redis', 'sidekiq'],
data() {
return { currentRequestId: '' };
},
computed: {
requests() {
return this.store.requestsWithDetails();
},
currentRequest: {
get() {
return this.store.findRequest(this.currentRequestId);
},
set(requestId) {
this.currentRequestId = requestId;
},
},
initialRequest() {
return this.currentRequestId === this.requestId;
},
lineProfileModal() {
return $('#modal-peek-line-profile');
},
},
mounted() {
this.interceptor = PerformanceBarService.registerInterceptor(
this.peekUrl,
this.loadRequestDetails,
);
this.loadRequestDetails(this.requestId, window.location.href);
this.currentRequest = this.requestId;
if (this.lineProfileModal.length) {
this.lineProfileModal.modal('toggle');
}
},
beforeDestroy() {
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: {
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
this.store.addRequest(requestId, requestUrl);
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
})
.catch(() =>
Flash(`Error getting performance bar results for ${requestId}`),
);
},
changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId;
},
},
};
</script>
<template>
<div
id="js-peek"
:class="env"
>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
:requests="requests"
@change-current-request="changeCurrentRequest"
/>
<div
id="peek-view-host"
class="view prepend-left-5"
>
<span
v-if="currentRequest && currentRequest.details"
class="current-host"
>
{{ currentRequest.details.host.hostname }}
</span>
</div>
<div
v-if="currentRequest"
class="wrapper"
>
<upstream-performance-bar
v-if="initialRequest && currentRequest.details"
/>
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
:current-request="currentRequest"
:metric="metric.metric"
:header="metric.header"
:details="metric.details"
:keys="metric.keys"
/>
<div
v-if="initialRequest"
id="peek-view-rblineprof"
class="view"
>
<button
v-if="lineProfileModal.length"
class="btn-link btn-blank"
data-toggle="modal"
data-target="#modal-peek-line-profile"
>
profile
</button>
<a
v-else
:href="profileUrl"
>
profile
</a>
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:current-request="currentRequest"
:key="metric"
:metric="metric"
/>
<div
id="peek-view-gc"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms
/
<span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span>
gc
</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
requests: {
type: Array,
required: true,
},
},
data() {
return {
currentRequestId: this.currentRequest.id,
};
},
watch: {
currentRequestId(newRequestId) {
this.$emit('change-current-request', newRequestId);
},
},
methods: {
truncatedUrl(requestUrl) {
const components = requestUrl.replace(/\/$/, '').split('/');
let truncated = components[components.length - 1];
if (truncated.match(/^\d+$/)) {
truncated = `${components[components.length - 2]}/${truncated}`;
}
return truncated;
},
},
};
</script>
<template>
<div
id="peek-request-selector"
class="append-right-5 pull-right"
>
<select v-model="currentRequestId">
<option
v-for="request in requests"
:key="request.id"
:value="request.id"
>
{{ truncatedUrl(request.url) }}
</option>
</select>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
{{ metric }}
</div>
</template>
<script>
export default {
mounted() {
const upstreamPerformanceBar = document
.getElementById('peek-view-performance-bar')
.cloneNode(true);
this.$refs.wrapper.appendChild(upstreamPerformanceBar);
},
};
</script>
<template>
<div
id="peek-view-performance-bar-vue"
class="view"
ref="wrapper"
></div>
</template>
import 'vendor/peek.performance_bar';
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
export default () =>
new Vue({
el: '#js-peek',
components: {
performanceBarApp,
},
data() {
const performanceBarData = document.querySelector(this.$options.el)
.dataset;
const store = new PerformanceBarStore();
return {
store,
env: performanceBarData.env,
requestId: performanceBarData.requestId,
peekUrl: performanceBarData.peekUrl,
profileUrl: performanceBarData.profileUrl,
};
},
render(createElement) {
return createElement('performance-bar-app', {
props: {
store: this.store,
env: this.env,
requestId: this.requestId,
peekUrl: this.peekUrl,
profileUrl: this.profileUrl,
},
});
},
});
import axios from '../../lib/utils/axios_utils';
export default class PerformanceBarService {
static fetchRequestDetails(peekUrl, requestId) {
return axios.get(peekUrl, { params: { request_id: requestId } });
}
static registerInterceptor(peekUrl, callback) {
return axios.interceptors.response.use(response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
if (requestUrl !== peekUrl && requestId) {
callback(requestId, requestUrl);
}
return response;
});
}
static removeInterceptor(interceptor) {
axios.interceptors.response.eject(interceptor);
}
}
export default class PerformanceBarStore {
constructor() {
this.requests = [];
}
addRequest(requestId, requestUrl, requestDetails) {
if (!this.findRequest(requestId)) {
this.requests.push({
id: requestId,
url: requestUrl,
details: requestDetails,
});
}
return this.requests;
}
findRequest(requestId) {
return this.requests.find(request => request.id === requestId);
}
addRequestDetails(requestId, requestDetails) {
const request = this.findRequest(requestId);
request.details = requestDetails;
return request;
}
requestsWithDetails() {
return this.requests.filter(request => request.details);
}
canTrackRequest(requestUrl) {
return (
this.requests.filter(request => request.url === requestUrl).length < 2
);
}
}
...@@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap'; ...@@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap';
import _ from 'underscore'; import _ from 'underscore';
import Sidebar from './right_sidebar'; import Sidebar from './right_sidebar';
import Shortcuts from './shortcuts'; import Shortcuts from './shortcuts';
import { CopyAsGFM } from './behaviors/copy_as_gfm'; import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm';
export default class ShortcutsIssuable extends Shortcuts { export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) { constructor(isMergeRequest) {
......
.navbar-gitlab { .navbar-gitlab {
&.navbar-gitlab {
padding: 0 16px; padding: 0 16px;
z-index: 1000; z-index: 1000;
margin-bottom: 0; margin-bottom: 0;
...@@ -23,41 +22,6 @@ ...@@ -23,41 +22,6 @@
} }
} }
.container-fluid {
padding: 0;
.user-counter {
svg {
margin-right: 3px;
}
}
.navbar-toggle {
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
.more-icon,
.close-icon {
fill: $white-light;
margin: auto;
}
}
}
}
.close-icon { .close-icon {
display: none; display: none;
} }
...@@ -184,6 +148,38 @@ ...@@ -184,6 +148,38 @@
} }
.container-fluid { .container-fluid {
padding: 0;
.user-counter {
svg {
margin-right: 3px;
}
}
.navbar-toggle {
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
.more-icon,
.close-icon {
fill: $white-light;
margin: auto;
}
}
.navbar-nav { .navbar-nav {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
display: -webkit-flex; display: -webkit-flex;
......
...@@ -140,12 +140,6 @@ ul.notes { ...@@ -140,12 +140,6 @@ ul.notes {
@include bulleted-list; @include bulleted-list;
word-wrap: break-word; word-wrap: break-word;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
table { table {
@include markdown-table; @include markdown-table;
} }
......
...@@ -68,7 +68,6 @@ ...@@ -68,7 +68,6 @@
.ide-new-btn { .ide-new-btn {
display: none; display: none;
margin-top: -4px;
margin-bottom: -4px; margin-bottom: -4px;
margin-right: -8px; margin-right: -8px;
} }
...@@ -84,7 +83,6 @@ ...@@ -84,7 +83,6 @@
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
} }
} }
a { a {
...@@ -290,7 +288,7 @@ ...@@ -290,7 +288,7 @@
.margin-view-overlays .insert-sign, .margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign { .margin-view-overlays .delete-sign {
opacity: .4; opacity: 0.4;
} }
} }
} }
...@@ -548,7 +546,6 @@ ...@@ -548,7 +546,6 @@
height: 10px; height: 10px;
margin-left: 3px; margin-left: 3px;
} }
} }
.multi-file-commit-list-path { .multi-file-commit-list-path {
...@@ -626,7 +623,7 @@ ...@@ -626,7 +623,7 @@
top: 0; top: 0;
width: 100px; width: 100px;
height: 1px; height: 1px;
background-color: rgba($red-500, .5); background-color: rgba($red-500, 0.5);
} }
} }
} }
...@@ -720,12 +717,13 @@ ...@@ -720,12 +717,13 @@
} }
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); height: calc(
100vh - #{$header-height + $performance-bar-height + $flash-height}
);
} }
} }
} }
.dragHandle { .dragHandle {
position: absolute; position: absolute;
top: 0; top: 0;
......
@import "framework/variables"; @import 'framework/variables';
@import "peek/views/performance_bar"; @import 'peek/views/performance_bar';
@import "peek/views/rblineprof"; @import 'peek/views/rblineprof';
#peek { #js-peek {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
...@@ -21,14 +21,26 @@ ...@@ -21,14 +21,26 @@
&.production { &.production {
background-color: $perf-bar-production; background-color: $perf-bar-production;
select {
background: $perf-bar-production;
}
} }
&.staging { &.staging {
background-color: $perf-bar-staging; background-color: $perf-bar-staging;
select {
background: $perf-bar-staging;
}
} }
&.development { &.development {
background-color: $perf-bar-development; background-color: $perf-bar-development;
select {
background: $perf-bar-development;
}
} }
.wrapper { .wrapper {
...@@ -42,11 +54,12 @@ ...@@ -42,11 +54,12 @@
background: $perf-bar-bucket-bg; background: $perf-bar-bucket-bg;
display: inline-block; display: inline-block;
padding: 4px 6px; padding: 4px 6px;
font-family: Consolas, "Liberation Mono", Courier, monospace; font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1; line-height: 1;
color: $perf-bar-bucket-color; color: $perf-bar-bucket-color;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
.hidden { .hidden {
display: none; display: none;
...@@ -94,6 +107,10 @@ ...@@ -94,6 +107,10 @@
max-width: 10000px !important; max-width: 10000px !important;
} }
} }
.performance-bar-modal .modal-footer {
display: none;
}
} }
#modal-peek-pg-queries-content { #modal-peek-pg-queries-content {
......
...@@ -16,6 +16,7 @@ class Admin::ProjectsFinder ...@@ -16,6 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items) items = by_archived(items)
items = by_personal(items) items = by_personal(items)
items = by_name(items) items = by_name(items)
items = items.includes(namespace: [:owner])
sort(items).page(params[:page]) sort(items).page(params[:page])
end end
......
...@@ -304,7 +304,7 @@ module ApplicationHelper ...@@ -304,7 +304,7 @@ module ApplicationHelper
def linkedin_url(user) def linkedin_url(user)
name = user.linkedin name = user.linkedin
if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/}
name name
else else
"https://www.linkedin.com/in/#{name}" "https://www.linkedin.com/in/#{name}"
...@@ -313,10 +313,10 @@ module ApplicationHelper ...@@ -313,10 +313,10 @@ module ApplicationHelper
def twitter_url(user) def twitter_url(user)
name = user.twitter name = user.twitter
if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} if name =~ %r{\Ahttps?://(www\.)?twitter\.com/}
name name
else else
"https://www.twitter.com/#{name}" "https://twitter.com/#{name}"
end end
end end
......
module ServicesHelper module ServicesHelper
prepend EE::ServicesHelper prepend EE::ServicesHelper
def service_event_description(event)
case event
when "push", "push_events"
"Event will be triggered by a push to the repository"
when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
when "note", "note_events"
"Event will be triggered when someone adds a comment"
when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "pipeline", "pipeline_events"
"Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
def service_event_field_name(event) def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events" "#{event}_events"
......
...@@ -35,7 +35,8 @@ class NotificationRecipient ...@@ -35,7 +35,8 @@ class NotificationRecipient
# check this last because it's expensive # check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed # nobody should receive notifications if they've specifically unsubscribed
return false if unsubscribed? # except if they were mentioned.
return false if @type != :mention && unsubscribed?
true true
end end
......
...@@ -14,9 +14,8 @@ class JiraService < IssueTrackerService ...@@ -14,9 +14,8 @@ class JiraService < IssueTrackerService
alias_method :project_url, :url alias_method :project_url, :url
# This is confusing, but JiraService does not really support these events. # When these are false GitLab does not create cross reference
# The values here are required to display correct options in the service # comments on JIRA except when an issue gets transitioned.
# configuration screen.
def self.supported_events def self.supported_events
%w(commit merge_request) %w(commit merge_request)
end end
...@@ -318,4 +317,13 @@ class JiraService < IssueTrackerService ...@@ -318,4 +317,13 @@ class JiraService < IssueTrackerService
url_changed? url_changed?
end end
def self.event_description(event)
case event
when "merge_request", "merge_request_events"
"JIRA comments will be created when an issue gets referenced in a merge request."
when "commit", "commit_events"
"JIRA comments will be created when an issue gets referenced in a commit."
end
end
end end
...@@ -309,6 +309,29 @@ class Service < ActiveRecord::Base ...@@ -309,6 +309,29 @@ class Service < ActiveRecord::Base
end end
end end
def self.event_description(event)
case event
when "push", "push_events"
"Event will be triggered by a push to the repository"
when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
when "note", "note_events"
"Event will be triggered when someone adds a comment"
when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "pipeline", "pipeline_events"
"Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
def valid_recipients? def valid_recipients?
activated? && !importing? activated? && !importing?
end end
......
...@@ -93,6 +93,7 @@ module Projects ...@@ -93,6 +93,7 @@ module Projects
# hook failed and caused us to end up here. A destroyed model will be a frozen hash, # hook failed and caused us to end up here. A destroyed model will be a frozen hash,
# which cannot be altered. # which cannot be altered.
project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed? project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
log_error("Deletion failed on #{project.full_path} with the following message: #{message}") log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end end
......
- return unless peek_enabled?
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
peek_url: peek_routes.results_url,
profile_url: url_for(params.merge(lineprofiler: 'true')) },
class: Peek.env }
#peek-view-performance-bar
= render_server_response_time
%span#serverstats
%ul.performance-bar
- local_assigns.fetch(:view)
%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-gitaly-details' } }
%span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-gitaly-details.modal{ tabindex: -1, role: 'dialog' }
.modal-dialog.modal-full
.modal-content
.modal-header
%button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
%span{ 'aria-hidden' => 'true' }
&times;
%h4
Gitaly requests
.modal-body{ data: { defer_to: "#{view.defer_key}-details" } }...
gitaly
%span.current-host
= truncate(view.hostname)
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
mysql
- local_assigns.fetch(:view)
= render 'peek/views/sql', view: view
pg
Profile:
= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile'
\/
= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile'
\/
= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile'
%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-pg-queries' } }
%span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/
%span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 }
.modal-dialog.modal-full
.modal-content
.modal-header
%button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
%span{ 'aria-hidden' => 'true' }
&times;
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-no-data-svg-path": image_path('illustrations/monitoring/no_data.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json), "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json), "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.light %p.light
= service_event_description(event) = @service.class.event_description(event)
- @service.global_fields.each do |field| - @service.global_fields.each do |field|
- type = field[:type] - type = field[:type]
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
= f.hidden_field :access_level = f.hidden_field :access_level
.member-form-control.dropdown.append-right-5 .member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: member.can_override?, disabled: member.can_override? && !member.override?,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
= member.human_access = member.human_access
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
can_override: member.can_override? can_override: member.can_override?
.prepend-left-5.clearable-input.member-form-control .prepend-left-5.clearable-input.member-form-control
= f.text_field :expires_at, = f.text_field :expires_at,
disabled: member.can_override?, disabled: member.can_override? && !member.override?,
class: 'form-control js-access-expiration-date js-member-update-control', class: 'form-control js-access-expiration-date js-member-update-control',
placeholder: 'Expiration date', placeholder: 'Expiration date',
id: "member_expires_at_#{member.id}", id: "member_expires_at_#{member.id}",
......
---
title: Send @mention notifications even if a user has explicitly unsubscribed from
item
merge_request:
author:
type: added
---
title: Fix search results stripping last endline when parsing the results
merge_request: 17777
author: Jasper Maes
type: fixed
---
title: Add documentation for displayed K8s Ingress IP address (#44330)
merge_request: 17836
author:
type: other
---
title: Clean up selectors in framework/header.scss
merge_request: 17822
author: Takuya Noguchi
type: other
---
title: Unify format for nested non-task lists
merge_request: 17823
author: Takuya Noguchi
type: fixed
---
title: Allow viewing timings for AJAX requests in the performance bar
merge_request:
author:
type: changed
---
title: Improve JIRA event descriptions
merge_request:
author:
type: other
---
title: Make /-/ delimiter optional for search endpoints
merge_request:
author:
type: changed
---
title: Fix "Can't modify frozen hash" error when project is destroyed
merge_request:
author:
type: fixed
# rubocop:disable Lint/RescueException # rubocop:disable Lint/RescueException
# This patch fixes https://github.com/rails/rails/issues/26024 # Remove this entire initializer when we are at rails 5.0.
# TODO: Remove it when it's no longer necessary # This file fixes the bug (see below) which has been fixed in the upstream.
unless Gitlab.rails5?
# This patch fixes https://github.com/rails/rails/issues/26024
# TODO: Remove it when it's no longer necessary
module ActiveRecord module ActiveRecord
module Locking module Locking
module Optimistic module Optimistic
# We overwrite this method because we don't want to have default value # We overwrite this method because we don't want to have default value
...@@ -71,4 +74,5 @@ module ActiveRecord ...@@ -71,4 +74,5 @@ module ActiveRecord
end end
end end
end end
end
end end
...@@ -59,7 +59,7 @@ Rails.application.routes.draw do ...@@ -59,7 +59,7 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness' get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check' post 'storage_check' => 'health#storage_check'
resources :metrics, only: [:index] resources :metrics, only: [:index]
mount Peek::Railtie => '/peek' mount Peek::Railtie => '/peek', as: 'peek_routes'
# Boards resources shared between group and projects # Boards resources shared between group and projects
resources :boards, only: [] do resources :boards, only: [] do
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180309160427) do ActiveRecord::Schema.define(version: 20180314100728) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -184,6 +184,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do ...@@ -184,6 +184,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.string "external_authorization_service_url" t.string "external_authorization_service_url"
t.string "external_authorization_service_default_label" t.string "external_authorization_service_default_label"
t.boolean "pages_domain_verification_enabled", default: true, null: false t.boolean "pages_domain_verification_enabled", default: true, null: false
t.float "external_authorization_service_timeout", default: 0.5, null: false
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -13,12 +13,16 @@ It allows you to see (from left to right): ...@@ -13,12 +13,16 @@ It allows you to see (from left to right):
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png) ![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
- time taken and number of [Gitaly] calls, click through for details of these calls - time taken and number of [Gitaly] calls, click through for details of these calls
![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png) ![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png)
- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)). - profile of the code used to generate the page, line by line. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)).
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png) ![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
- time taken and number of calls to Redis - time taken and number of calls to Redis
- time taken and number of background jobs created by Sidekiq - time taken and number of background jobs created by Sidekiq
- time taken and number of Ruby GC calls - time taken and number of Ruby GC calls
On the far right is a request selector that allows you to view the same metrics
(excluding the page timing and line profiler) for any requests made while the
page was open. Only the first two requests per unique URL are captured.
## Enable the Performance Bar via the Admin panel ## Enable the Performance Bar via the Admin panel
GitLab Performance Bar is disabled by default. To enable it for a given group, GitLab Performance Bar is disabled by default. To enable it for a given group,
......
...@@ -10,7 +10,7 @@ Epics are available only in Ultimate. If epics feature is not available a `403` ...@@ -10,7 +10,7 @@ Epics are available only in Ultimate. If epics feature is not available a `403`
Gets all issues that are assigned to an epic and the authenticated user has access to. Gets all issues that are assigned to an epic and the authenticated user has access to.
``` ```
GET /groups/:id/-/epics/:epic_iid/issues GET /groups/:id/epics/:epic_iid/issues
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -19,7 +19,7 @@ GET /groups/:id/-/epics/:epic_iid/issues ...@@ -19,7 +19,7 @@ GET /groups/:id/-/epics/:epic_iid/issues
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer/string | yes | The internal ID of the epic. |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5/issues/ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/
``` ```
Example response: Example response:
...@@ -106,7 +106,7 @@ Example response: ...@@ -106,7 +106,7 @@ Example response:
Creates an epic - issue association. If the issue in question belongs to another epic it is unassigned from that epic. Creates an epic - issue association. If the issue in question belongs to another epic it is unassigned from that epic.
``` ```
POST /groups/:id/-/epics/:epic_iid/issues/:issue_id POST /groups/:id/epics/:epic_iid/issues/:issue_id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -116,7 +116,7 @@ POST /groups/:id/-/epics/:epic_iid/issues/:issue_id ...@@ -116,7 +116,7 @@ POST /groups/:id/-/epics/:epic_iid/issues/:issue_id
| `issue_id` | integer/string | yes | The ID of the issue. | | `issue_id` | integer/string | yes | The ID of the issue. |
```bash ```bash
curl --header POST "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5/issues/55 curl --header POST "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/55
``` ```
Example response: Example response:
...@@ -212,7 +212,7 @@ Example response: ...@@ -212,7 +212,7 @@ Example response:
Removes an epic - issue association. Removes an epic - issue association.
``` ```
DELETE /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id DELETE /groups/:id/epics/:epic_iid/issues/:epic_issue_id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -222,7 +222,7 @@ DELETE /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id ...@@ -222,7 +222,7 @@ DELETE /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id
| `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. | | `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. |
```bash ```bash
curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5/issues/11 curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11
``` ```
Example response: Example response:
...@@ -318,7 +318,7 @@ Example response: ...@@ -318,7 +318,7 @@ Example response:
Updates an epic - issue association. Updates an epic - issue association.
``` ```
PUT /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id PUT /groups/:id/epics/:epic_iid/issues/:epic_issue_id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -330,7 +330,7 @@ PUT /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id ...@@ -330,7 +330,7 @@ PUT /groups/:id/-/epics/:epic_iid/issues/:epic_issue_id
| `move_after_id` | integer/string | no | The ID of the issue - epic association that should be placed after the link in the question. | | `move_after_id` | integer/string | no | The ID of the issue - epic association that should be placed after the link in the question. |
```bash ```bash
curl --header PUT "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5/issues/11?move_before_id=20 curl --header PUT "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11?move_before_id=20
``` ```
Example response: Example response:
......
...@@ -15,9 +15,9 @@ The [epic issues API](epic_issues.md) allows you to interact with issues associa ...@@ -15,9 +15,9 @@ The [epic issues API](epic_issues.md) allows you to interact with issues associa
Gets all epics of the requested group and its subgroups. Gets all epics of the requested group and its subgroups.
``` ```
GET /groups/:id/-/epics GET /groups/:id/epics
GET /groups/:id/-/epics?author_id=5 GET /groups/:id/epics?author_id=5
GET /groups/:id/-/epics?labels=bug,reproduced GET /groups/:id/epics?labels=bug,reproduced
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -30,7 +30,7 @@ GET /groups/:id/-/epics?labels=bug,reproduced ...@@ -30,7 +30,7 @@ GET /groups/:id/-/epics?labels=bug,reproduced
| `search` | string | no | Search epics against their `title` and `description` | | `search` | string | no | Search epics against their `title` and `description` |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics
``` ```
Example response: Example response:
...@@ -65,7 +65,7 @@ Example response: ...@@ -65,7 +65,7 @@ Example response:
Gets a single epic Gets a single epic
``` ```
GET /groups/:id/-/epics/:epic_iid GET /groups/:id/epics/:epic_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -74,7 +74,7 @@ GET /groups/:id/-/epics/:epic_iid ...@@ -74,7 +74,7 @@ GET /groups/:id/-/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer/string | yes | The internal ID of the epic. |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5
``` ```
Example response: Example response:
...@@ -106,7 +106,7 @@ Example response: ...@@ -106,7 +106,7 @@ Example response:
Creates a new epic Creates a new epic
``` ```
POST /groups/:id/-/epics POST /groups/:id/epics
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -119,7 +119,7 @@ POST /groups/:id/-/epics ...@@ -119,7 +119,7 @@ POST /groups/:id/-/epics
| `end_date` | string. | no | The end date of the epic | | `end_date` | string. | no | The end date of the epic |
```bash ```bash
curl --header POST "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics?title=Epic&description=Epic%20description curl --header POST "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics?title=Epic&description=Epic%20description
``` ```
Example response: Example response:
...@@ -152,7 +152,7 @@ Example response: ...@@ -152,7 +152,7 @@ Example response:
Updates an epic Updates an epic
``` ```
PUT /groups/:id/-/epics/:epic_iid PUT /groups/:id/epics/:epic_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -166,7 +166,7 @@ PUT /groups/:id/-/epics/:epic_iid ...@@ -166,7 +166,7 @@ PUT /groups/:id/-/epics/:epic_iid
| `end_date` | string. | no | The end date of an epic | | `end_date` | string. | no | The end date of an epic |
```bash ```bash
curl --header PUT "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5?title=New%20Title curl --header PUT "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title
``` ```
Example response: Example response:
...@@ -199,7 +199,7 @@ Example response: ...@@ -199,7 +199,7 @@ Example response:
Deletes an epic Deletes an epic
``` ```
DELETE /groups/:id/-/epics/:epic_iid DELETE /groups/:id/epics/:epic_iid
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -208,5 +208,5 @@ DELETE /groups/:id/-/epics/:epic_iid ...@@ -208,5 +208,5 @@ DELETE /groups/:id/-/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic. | | `epic_iid` | integer/string | yes | The internal ID of the epic. |
```bash ```bash
curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5?title=New%20Title curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title
``` ```
...@@ -374,7 +374,7 @@ Search within the specified group. ...@@ -374,7 +374,7 @@ Search within the specified group.
If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code. If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
``` ```
GET /groups/:id/-/search GET /groups/:id/search
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -392,7 +392,7 @@ The response depends on the requested scope. ...@@ -392,7 +392,7 @@ The response depends on the requested scope.
### Scope: projects ### Scope: projects
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=projects&search=flight curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=projects&search=flight
``` ```
Example response: Example response:
...@@ -423,7 +423,7 @@ Example response: ...@@ -423,7 +423,7 @@ Example response:
### Scope: issues ### Scope: issues
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=issues&search=file curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=issues&search=file
``` ```
Example response: Example response:
...@@ -488,7 +488,7 @@ Example response: ...@@ -488,7 +488,7 @@ Example response:
### Scope: merge_requests ### Scope: merge_requests
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=merge_requests&search=file curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=merge_requests&search=file
``` ```
Example response: Example response:
...@@ -565,7 +565,7 @@ Example response: ...@@ -565,7 +565,7 @@ Example response:
### Scope: milestones ### Scope: milestones
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=milestones&search=release curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=milestones&search=release
``` ```
Example response: Example response:
...@@ -592,7 +592,7 @@ Example response: ...@@ -592,7 +592,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled. This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/-/search?scope=wiki_blobs&search=bye curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/search?scope=wiki_blobs&search=bye
``` ```
Example response: Example response:
...@@ -617,7 +617,7 @@ Example response: ...@@ -617,7 +617,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled. This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/-/search?scope=commits&search=bye curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/search?scope=commits&search=bye
``` ```
Example response: Example response:
...@@ -650,7 +650,7 @@ Example response: ...@@ -650,7 +650,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled. This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/-/search?scope=blobs&search=installation curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/6/search?scope=blobs&search=installation
``` ```
Example response: Example response:
...@@ -677,7 +677,7 @@ Search within the specified project. ...@@ -677,7 +677,7 @@ Search within the specified project.
If a user is not a member of a project and the project is private, a `GET` request on that project will result to a `404` status code. If a user is not a member of a project and the project is private, a `GET` request on that project will result to a `404` status code.
``` ```
GET /projects/:id/-/search GET /projects/:id/search
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -694,7 +694,7 @@ The response depends on the requested scope. ...@@ -694,7 +694,7 @@ The response depends on the requested scope.
### Scope: issues ### Scope: issues
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=issues&search=file curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=issues&search=file
``` ```
Example response: Example response:
...@@ -759,7 +759,7 @@ Example response: ...@@ -759,7 +759,7 @@ Example response:
### Scope: merge_requests ### Scope: merge_requests
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=merge_requests&search=file curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=merge_requests&search=file
``` ```
Example response: Example response:
...@@ -836,7 +836,7 @@ Example response: ...@@ -836,7 +836,7 @@ Example response:
### Scope: milestones ### Scope: milestones
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=milestones&search=release curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=milestones&search=release
``` ```
Example response: Example response:
...@@ -861,7 +861,7 @@ Example response: ...@@ -861,7 +861,7 @@ Example response:
### Scope: notes ### Scope: notes
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=notes&search=maxime curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=notes&search=maxime
``` ```
Example response: Example response:
...@@ -893,7 +893,7 @@ Example response: ...@@ -893,7 +893,7 @@ Example response:
### Scope: wiki_blobs ### Scope: wiki_blobs
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=wiki_blobs&search=bye curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
``` ```
Example response: Example response:
...@@ -916,7 +916,7 @@ Example response: ...@@ -916,7 +916,7 @@ Example response:
### Scope: commits ### Scope: commits
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=commits&search=bye curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=commits&search=bye
``` ```
Example response: Example response:
...@@ -947,7 +947,7 @@ Example response: ...@@ -947,7 +947,7 @@ Example response:
### Scope: blobs ### Scope: blobs
```bash ```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=blobs&search=installation curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
``` ```
Example response: Example response:
......
...@@ -173,6 +173,7 @@ PUT /application/settings ...@@ -173,6 +173,7 @@ PUT /application/settings
| `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects | | `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects |
| `external_authorization_service_url` | string | no | URL to which authorization requests will be directed | | `external_authorization_service_url` | string | no | URL to which authorization requests will be directed |
| `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project | | `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project |
| `external_authorization_service_timeout` | float | no | The timeout to enforce when performing requests to the external authorization service |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
...@@ -213,6 +214,7 @@ Example response: ...@@ -213,6 +214,7 @@ Example response:
"polling_interval_multiplier": 1.0, "polling_interval_multiplier": 1.0,
"external_authorization_service_enabled": true, "external_authorization_service_enabled": true,
"external_authorization_service_url": "https://authorize.me", "external_authorization_service_url": "https://authorize.me",
"external_authorization_service_default_label": "default" "external_authorization_service_default_label": "default",
"external_authorization_service_timeout": 0.5
} }
``` ```
...@@ -37,7 +37,7 @@ By offering individual containers and charts, we will be able to provide a numbe ...@@ -37,7 +37,7 @@ By offering individual containers and charts, we will be able to provide a numbe
This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017. This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017.
Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
## Other Charts ## Other Charts
......
...@@ -38,6 +38,9 @@ admin area under the settings page: ...@@ -38,6 +38,9 @@ admin area under the settings page:
The available required properties are: The available required properties are:
- **Service URL**: The URL to make authorization requests to - **Service URL**: The URL to make authorization requests to
- **External authorization request timeout**: The timeout after which an
authorization request is aborted. When a request times out, access is denied
to the user.
- **Default classification label**: The classification label to use when - **Default classification label**: The classification label to use when
requesting authorization if no specific label is defined on the project requesting authorization if no specific label is defined on the project
......
...@@ -117,7 +117,7 @@ are trusted, so **only trusted users should be allowed to control your clusters* ...@@ -117,7 +117,7 @@ are trusted, so **only trusted users should be allowed to control your clusters*
The default cluster configuration grants access to a wide set of The default cluster configuration grants access to a wide set of
functionalities needed to successfully build and deploy a containerized functionalities needed to successfully build and deploy a containerized
application. Bare in mind that the same credentials are used for all the application. Bear in mind that the same credentials are used for all the
applications running on the cluster. applications running on the cluster.
When GitLab creates the cluster, it enables and uses the legacy When GitLab creates the cluster, it enables and uses the legacy
...@@ -167,6 +167,17 @@ external IP address with the following procedure. It can be deployed using the ...@@ -167,6 +167,17 @@ external IP address with the following procedure. It can be deployed using the
In order to publish your web application, you first need to find the external IP In order to publish your web application, you first need to find the external IP
address associated to your load balancer. address associated to your load balancer.
### Let GitLab fetch the IP address
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6.
If you installed the Ingress [via the **Applications**](#installing-applications),
you should see the Ingress IP address on this same page within a few minutes.
If you don't see this, GitLab might not be able to determine the IP address of
your ingress application in which case you should manually determine it.
### Manually determining the IP address
If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the **Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/) [Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
...@@ -193,6 +204,24 @@ The output is the external IP address of your cluster. This information can then ...@@ -193,6 +204,24 @@ The output is the external IP address of your cluster. This information can then
be used to set up DNS entries and forwarding rules that allow external access to be used to set up DNS entries and forwarding rules that allow external access to
your deployed applications. your deployed applications.
### Using a static IP
By default, an ephemeral external IP address is associated to the cluster's load
balancer. If you associate the ephemeral IP with your DNS and the IP changes,
your apps will not be able to be reached, and you'd have to change the DNS
record again. In order to avoid that, you should change it into a static
reserved IP.
[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip)
### Pointing your DNS at the cluster IP
Once you've set up the static IP, you should associate it to a [wildcard DNS
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able
to reach your apps. This heavily depends on your domain provider, but in case
you aren't sure, just create an A record with a wildcard host like
`*.example.com.`.
## Setting the environment scope ## Setting the environment scope
NOTE: **Note:** NOTE: **Note:**
...@@ -279,6 +308,14 @@ GitLab CI/CD build environment. ...@@ -279,6 +308,14 @@ GitLab CI/CD build environment.
| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | | `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | | `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
## Monitoring your Kubernetes cluster
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4701) in [GitLab Ultimate][ee] 10.6.
When [Prometheus is deployed](#installing-applications), GitLab will automatically monitor the cluster's health. At the top of the cluster settings page, CPU and Memory utilization is displayed, along with the total amount available. If the cluster runs out of memory, pods will become to be shutdown or fail to start.
![Cluster Monitoring](img/k8s_cluster_monitoring.png)
## Enabling or disabling the Kubernetes cluster integration ## Enabling or disabling the Kubernetes cluster integration
After you have successfully added your cluster information, you can enable the After you have successfully added your cluster information, you can enable the
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
> [Introduced][ce-8935] in GitLab 9.0. > [Introduced][ce-8935] in GitLab 9.0.
GitLab offers powerful integration with [Prometheus] for monitoring key metrics your apps, directly within GitLab. GitLab offers powerful integration with [Prometheus] for monitoring key metrics of your apps, directly within GitLab.
Metrics for each environment are retrieved from Prometheus, and then displayed Metrics for each environment are retrieved from Prometheus, and then displayed
within the GitLab interface. within the GitLab interface.
...@@ -12,17 +12,21 @@ There are two ways to setup Prometheus integration, depending on where your apps ...@@ -12,17 +12,21 @@ There are two ways to setup Prometheus integration, depending on where your apps
* For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes) * For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes)
* For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus). * For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus).
## Managed Prometheus on Kubernetes Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-ci-cd-environments). You are also able to [add your own metrics](#adding-additional-metrics) as well.
## Enabling Prometheus Integration
### Managed Prometheus on Kubernetes
> **Note**: [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) in GitLab 10.5 > **Note**: [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) in GitLab 10.5
GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.md), making monitoring of your apps easy. GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.md), making monitoring of your apps easy.
### Requirements #### Requirements
* A [connected Kubernetes cluster](../clusters/index.md) * A [connected Kubernetes cluster](../clusters/index.md)
* Helm Tiller [installed by GitLab](../clusters/index.md#installing-applications) * Helm Tiller [installed by GitLab](../clusters/index.md#installing-applications)
### Getting started #### Getting started
Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click. Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click.
...@@ -32,7 +36,7 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma ...@@ -32,7 +36,7 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma
![Managed Prometheus Deploy](img/prometheus_deploy.png) ![Managed Prometheus Deploy](img/prometheus_deploy.png)
### About managed Prometheus deployments #### About managed Prometheus deployments
Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/). Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/).
...@@ -45,9 +49,9 @@ CPU and Memory consumption is monitored, but requires [naming conventions](prome ...@@ -45,9 +49,9 @@ CPU and Memory consumption is monitored, but requires [naming conventions](prome
The [NGINX Ingress](../clusters/index.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates. The [NGINX Ingress](../clusters/index.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates.
## Manual configuration of Prometheus ### Manual configuration of Prometheus
### Requirements #### Requirements
Integration with Prometheus requires the following: Integration with Prometheus requires the following:
...@@ -56,7 +60,7 @@ Integration with Prometheus requires the following: ...@@ -56,7 +60,7 @@ Integration with Prometheus requires the following:
1. Each metric must be have a label to indicate the environment 1. Each metric must be have a label to indicate the environment
1. GitLab must have network connectivity to the Prometheus server 1. GitLab must have network connectivity to the Prometheus server
### Getting started #### Getting started
Installing and configuring Prometheus to monitor applications is fairly straight forward. Installing and configuring Prometheus to monitor applications is fairly straight forward.
...@@ -64,7 +68,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight ...@@ -64,7 +68,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
1. Set up one of the [supported monitoring targets](prometheus_library/metrics.md) 1. Set up one of the [supported monitoring targets](prometheus_library/metrics.md)
1. Configure the Prometheus server to [collect their metrics](https://prometheus.io/docs/operating/configuration/#scrape_config) 1. Configure the Prometheus server to [collect their metrics](https://prometheus.io/docs/operating/configuration/#scrape_config)
### Configuration in GitLab #### Configuration in GitLab
The actual configuration of Prometheus integration within GitLab is very simple. The actual configuration of Prometheus integration within GitLab is very simple.
All you will need is the DNS or IP address of the Prometheus server you'd like All you will need is the DNS or IP address of the Prometheus server you'd like
...@@ -83,9 +87,33 @@ to integrate with. ...@@ -83,9 +87,33 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment. environment which has had a successful deployment.
GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html). GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
### Adding additional metrics
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3799) in [GitLab Premium](https://about.gitlab.com/products/) 10.6.
Additional metrics can be monitored by adding them on the Prometheus integration page. Once saved, they will be displayed on the environment performance dashboard.
![Add New Metric](img/prometheus_add_metric.png)
A few fields are required:
* **Name**: Chart title
* **Type**: Type of metric. Metrics of the same type will be shown together.
* **Query**: Valid [PromQL query](https://prometheus.io/docs/prometheus/latest/querying/basics/). Note, no validation is performed at this time. If the query is not valid, the dashboard will display an error.
* **Y-axis label**: Y axis title to display on the dashboard.
* **Unit label**: Query units, for example `req / sec`. Shown next to the value.
#### Query Variables
GitLab supports a limited set of [CI variables] in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `CI_ENVIRONMENT_SLUG`. The supported variables are:
* CI_ENVIRONMENT_SLUG
* KUBE_NAMESPACE
[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments) To specify a variable in a query, enclose it in curly braces with a leading percent. For example: `%{ci_environment_slug}`.
## Determining the performance impact of a merge ## Determining the performance impact of a merge
...@@ -93,7 +121,7 @@ GitLab will automatically scan the Prometheus server for known metrics and attem ...@@ -93,7 +121,7 @@ GitLab will automatically scan the Prometheus server for known metrics and attem
> GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages. > GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages.
> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics > Requires [Kubernetes](prometheus_library/kubernetes.md) metrics
Developers can view theperformance impact of their changes within the merge Developers can view the performance impact of their changes within the merge
request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot
indicates when the current changes were deployed, with up to 30 minutes of indicates when the current changes were deployed, with up to 30 minutes of
performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after
...@@ -109,7 +137,7 @@ Prometheus server. ...@@ -109,7 +137,7 @@ Prometheus server.
## Troubleshooting ## Troubleshooting
If the "Attempting to load performance data" screen continues to appear, it could be due to: If the "No data found" screen continues to appear, it could be due to:
- No successful deployments have occurred to this environment. - No successful deployments have occurred to this environment.
- Prometheus does not have performance data for this environment, or the metrics - Prometheus does not have performance data for this environment, or the metrics
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<button <button
type="button" type="button"
class="btn btn-blank multi-file-discard-btn" class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file)" @click="discardFileChanges(file.path)"
> >
Discard Discard
</button> </button>
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
...@@ -16,7 +15,6 @@ ...@@ -16,7 +15,6 @@
repoFileButtons, repoFileButtons,
ideStatusBar, ideStatusBar,
repoEditor, repoEditor,
repoPreview,
}, },
props: { props: {
emptyStateSvgPath: { emptyStateSvgPath: {
...@@ -33,18 +31,12 @@ ...@@ -33,18 +31,12 @@
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(['changedFiles', 'openFiles', 'viewer']),
'currentBlobView', ...mapGetters(['activeFile', 'hasChanges']),
'selectedFile',
'changedFiles',
]),
...mapGetters([
'activeFile',
]),
}, },
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => { window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined; if (!this.changedFiles.length) return undefined;
Object.assign(e, { Object.assign(e, {
...@@ -67,20 +59,29 @@ ...@@ -67,20 +59,29 @@
<template <template
v-if="activeFile" v-if="activeFile"
> >
<repo-tabs/> <repo-tabs
<component :files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/>
<repo-editor
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
:is="currentBlobView" :file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/> />
<repo-file-buttons />
<ide-status-bar <ide-status-bar
:file="selectedFile" :file="activeFile"
/> />
</template> </template>
<template <template
v-else v-else
> >
<div class="ide-empty-state"> <div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state"> <div class="row js-empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content svg-250"> <div class="svg-content svg-250">
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue'; import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default { export default {
components: { components: {
repoCommitSection, repoCommitSection,
icon, icon,
panelResizer, panelResizer,
ResizablePanel,
}, },
props: { props: {
noChangesStateSvgPath: { noChangesStateSvgPath: {
...@@ -20,63 +22,21 @@ ...@@ -20,63 +22,21 @@
required: true, required: true,
}, },
}, },
data() {
return {
width: 340,
};
},
computed: { computed: {
...mapState([ ...mapState(['changedFiles', 'rightPanelCollapsed']),
'rightPanelCollapsed', ...mapGetters(['currentIcon']),
'changedFiles',
]),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.rightPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
}, },
methods: { methods: {
...mapActions([ ...mapActions(['setPanelCollapsedStatus']),
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
}, },
}, };
};
</script> </script>
<template> <template>
<div <resizable-panel
class="multi-file-commit-panel" :collapsible="true"
:class="{ :initial-width="340"
'is-collapsed': rightPanelCollapsed, side="right"
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
> >
<div <div
class="multi-file-commit-panel-section" class="multi-file-commit-panel-section"
...@@ -104,7 +64,10 @@ ...@@ -104,7 +64,10 @@
<button <button
type="button" type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn" class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="toggleCollapsed" @click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
> >
<icon <icon
:name="currentIcon" :name="currentIcon"
...@@ -117,15 +80,5 @@ ...@@ -117,15 +80,5 @@
:committed-state-svg-path="committedStateSvgPath" :committed-state-svg-path="committedStateSvgPath"
/> />
</div> </div>
<panel-resizer </resizable-panel>
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="340"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"
/>
</div>
</template> </template>
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue'; import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue'; import newDropdown from './new_dropdown/index.vue';
export default { export default {
components: { components: {
repoTree, repoTree,
icon, icon,
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
required: true, required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -40,8 +40,8 @@ export default { ...@@ -40,8 +40,8 @@ export default {
/> />
</div> </div>
</div> </div>
<div> <repo-tree
<repo-tree :tree-id="branch.treeId" /> :tree="branch.tree"
</div> />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import RepoFile from './repo_file.vue';
import repoFile from './repo_file.vue';
export default { export default {
components: { components: {
repoFile, RepoFile,
skeletonLoadingContainer, SkeletonLoadingContainer,
}, },
props: { props: {
treeId: { tree: {
type: String, type: Object,
required: true, required: true,
}, },
}, },
computed: {
...mapState([
'trees',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
selctedTree() {
return this.trees[this.treeId].tree;
},
showLoading() {
return !this.trees[this.treeId] || this.trees[this.treeId].loading;
},
},
}; };
</script> </script>
<template> <template>
<div <div
class="ide-file-list" class="ide-file-list"
v-if="treeId"
> >
<template v-if="showLoading"> <template v-if="tree.loading">
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
...@@ -47,10 +29,13 @@ export default { ...@@ -47,10 +29,13 @@ export default {
<skeleton-loading-container /> <skeleton-loading-container />
</div> </div>
</template> </template>
<template v-else>
<repo-file <repo-file
v-for="file in selctedTree" v-for="file in tree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0"
/> />
</template>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue'; import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
export default { export default {
components: { components: {
...@@ -11,65 +12,27 @@ ...@@ -11,65 +12,27 @@
icon, icon,
panelResizer, panelResizer,
skeletonLoadingContainer, skeletonLoadingContainer,
}, ResizablePanel,
data() {
return {
width: 290,
};
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading', 'loading',
'projects',
'leftPanelCollapsed',
]), ]),
currentIcon() { ...mapGetters([
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; 'projectsWithTrees',
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
showLoading() {
return this.loading;
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]), ]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
}, },
}; };
</script> </script>
<template> <template>
<div <resizable-panel
class="multi-file-commit-panel" :collapsible="false"
:class="{ :initial-width="290"
'is-collapsed': leftPanelCollapsed, side="left"
}"
:style="panelStyle"
> >
<div class="multi-file-commit-panel-inner"> <div class="multi-file-commit-panel-inner">
<template v-if="showLoading"> <template v-if="loading">
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
...@@ -79,36 +42,10 @@ ...@@ -79,36 +42,10 @@
</div> </div>
</template> </template>
<project-tree <project-tree
v-for="project in projects" v-for="project in projectsWithTrees"
:key="project.id" :key="project.id"
:project="project" :project="project"
/> />
</div> </div>
<button </resizable-panel>
type="button"
class="btn btn-transparent left-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>
Collapse sidebar
</span>
</button>
<panel-resizer
:size.sync="width"
:enabled="!leftPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"
/>
</div>
</template> </template>
<script> <script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
...@@ -20,11 +19,6 @@ ...@@ -20,11 +19,6 @@
required: true, required: true,
}, },
}, },
computed: {
...mapState([
'selectedFile',
]),
},
}; };
</script> </script>
...@@ -35,32 +29,32 @@ ...@@ -35,32 +29,32 @@
name="branch" name="branch"
:size="12" :size="12"
/> />
{{ selectedFile.branchId }} {{ file.branchId }}
</div> </div>
<div> <div>
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> <div v-if="file.lastCommit && file.lastCommit.id">
Last commit: Last commit:
<a <a
v-tooltip v-tooltip
:title="selectedFile.lastCommit.message" :title="file.lastCommit.message"
:href="selectedFile.lastCommit.url" :href="file.lastCommit.url"
> >
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by {{ timeFormated(file.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }} {{ file.lastCommit.author }}
</a> </a>
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
{{ selectedFile.name }} {{ file.name }}
</div> </div>
<div class="text-right"> <div class="text-right">
{{ selectedFile.eol }} {{ file.eol }}
</div> </div>
<div class="text-right"> <div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div class="text-right">
{{ selectedFile.fileLanguage }} {{ file.fileLanguage }}
</div> </div>
</div> </div>
</template> </template>
<script>
import $ from 'jquery';
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '~/flash';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
...mapState([
'currentBranch',
]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
},
methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
this.createNewBranch(this.branchName)
.then(() => {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
},
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>
<script> <script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
...@@ -18,10 +19,6 @@ ...@@ -18,10 +19,6 @@
type: String, type: String,
required: true, required: true,
}, },
parent: {
type: Object,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -31,6 +28,9 @@ ...@@ -31,6 +28,9 @@
}; };
}, },
methods: { methods: {
...mapActions([
'createTempEntry',
]),
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
this.openModal = true; this.openModal = true;
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
<upload <upload
:branch-id="branch" :branch-id="branch"
:path="path" :path="path"
:parent="parent" @create="createTempEntry"
/> />
</li> </li>
<li> <li>
...@@ -104,8 +104,8 @@ ...@@ -104,8 +104,8 @@
:type="modalType" :type="modalType"
:branch-id="branch" :branch-id="branch"
:path="path" :path="path"
:parent="parent"
@hide="hideModal" @hide="hideModal"
@create="createTempEntry"
/> />
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import modal from '~/vue_shared/components/modal.vue'; import modal from '~/vue_shared/components/modal.vue';
...@@ -12,10 +11,6 @@ ...@@ -12,10 +11,6 @@
type: String, type: String,
required: true, required: true,
}, },
parent: {
type: Object,
default: null,
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -31,9 +26,6 @@ ...@@ -31,9 +26,6 @@
}; };
}, },
computed: { computed: {
...mapState([
'currentProjectId',
]),
modalTitle() { modalTitle() {
if (this.type === 'tree') { if (this.type === 'tree') {
return __('Create new directory'); return __('Create new directory');
...@@ -60,15 +52,10 @@ ...@@ -60,15 +52,10 @@
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
}, },
methods: { methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() { createEntryInStore() {
this.createTempEntry({ this.$emit('create', {
projectId: this.currentProjectId,
branchId: this.branchId, branchId: this.branchId,
parent: this.parent, name: this.entryName,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type, type: this.type,
}); });
......
<script> <script>
import { mapActions, mapState } from 'vuex';
export default { export default {
props: { props: {
branchId: { branchId: {
type: String, type: String,
required: true, required: true,
}, },
parent: { path: {
type: Object, type: String,
default: null, required: false,
}, default: '',
}, },
computed: {
...mapState([
'trees',
'currentProjectId',
]),
}, },
mounted() { mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile); this.$refs.fileUpload.addEventListener('change', this.openFile);
...@@ -25,9 +18,6 @@ ...@@ -25,9 +18,6 @@
this.$refs.fileUpload.removeEventListener('change', this.openFile); this.$refs.fileUpload.removeEventListener('change', this.openFile);
}, },
methods: { methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) { createFile(target, file, isText) {
const { name } = file; const { name } = file;
let { result } = target; let { result } = target;
...@@ -36,11 +26,9 @@ ...@@ -36,11 +26,9 @@
result = result.split('base64,')[1]; result = result.split('base64,')[1];
} }
this.createTempEntry({ this.$emit('create', {
name, name: `${(this.path ? `${this.path}/` : '')}${name}`,
projectId: this.currentProjectId,
branchId: this.branchId, branchId: this.branchId,
parent: this.parent,
type: 'blob', type: 'blob',
content: result, content: result,
base64: !isText, base64: !isText,
......
...@@ -53,7 +53,6 @@ export default { ...@@ -53,7 +53,6 @@ export default {
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'getTreeData',
'setPanelCollapsedStatus', 'setPanelCollapsedStatus',
]), ]),
...mapActions('commit', [ ...mapActions('commit', [
......
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import modal from '~/vue_shared/components/modal.vue';
export default {
components: {
modal,
},
computed: {
...mapState([
'editMode',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
},
methods: {
...mapActions([
'toggleEditMode',
]),
},
};
</script>
<template>
<div class="editable-mode">
<button
v-if="canEditFile"
class="btn btn-default"
type="button"
@click.prevent="toggleEditMode()">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{ buttonLabel }}
</span>
</button>
</div>
</template>
<script> <script>
/* global monaco */ /* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
export default { export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: { computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([ ...mapState([
'leftPanelCollapsed', 'leftPanelCollapsed',
'rightPanelCollapsed', 'rightPanelCollapsed',
'panelResizing',
'viewer', 'viewer',
'delayViewerUpdated', 'delayViewerUpdated',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw; return this.file && this.file.binary && !this.file.raw;
}, },
}, },
watch: { watch: {
activeFile(oldVal, newVal) { file(oldVal, newVal) {
if (newVal && !newVal.active) { if (newVal.path !== this.file.path) {
this.initMonaco(); this.initMonaco();
} }
}, },
...@@ -34,11 +35,6 @@ export default { ...@@ -34,11 +35,6 @@ export default {
rightPanelCollapsed() { rightPanelCollapsed() {
this.editor.updateDimensions(); this.editor.updateDimensions();
}, },
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
viewer() { viewer() {
this.createEditorInstance(); this.createEditorInstance();
}, },
...@@ -72,7 +68,7 @@ export default { ...@@ -72,7 +68,7 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getRawFileData(this.activeFile) this.getRawFileData(this.file)
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve(); const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
...@@ -101,9 +97,9 @@ export default { ...@@ -101,9 +97,9 @@ export default {
}); });
}, },
setupEditor() { setupEditor() {
if (!this.activeFile || !this.editor.instance) return; if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile); this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model); this.editor.attachModel(this.model);
...@@ -112,7 +108,7 @@ export default { ...@@ -112,7 +108,7 @@ export default {
if (file.active) { if (file.active) {
this.changeFileContent({ this.changeFileContent({
file, path: file.path,
content: model.getModel().getValue(), content: model.getModel().getValue(),
}); });
} }
...@@ -127,8 +123,8 @@ export default { ...@@ -127,8 +123,8 @@ export default {
}); });
this.editor.setPosition({ this.editor.setPosition({
lineNumber: this.activeFile.editorRow, lineNumber: this.file.editorRow,
column: this.activeFile.editorColumn, column: this.file.editorColumn,
}); });
// Handle File Language // Handle File Language
...@@ -152,7 +148,7 @@ export default { ...@@ -152,7 +148,7 @@ export default {
> >
<div <div
v-if="shouldHideEditor" v-if="shouldHideEditor"
v-html="activeFile.html" v-html="file.html"
> >
</div> </div>
<div <div
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; export default {
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from 'ee/ide/components/repo_file_status_icon.vue'; // eslint-disable-line import/first
import changedFileIcon from 'ee/ide/components/changed_file_icon.vue'; // eslint-disable-line import/first
export default {
name: 'RepoFile', name: 'RepoFile',
components: { components: {
skeletonLoadingContainer, skeletonLoadingContainer,
...@@ -18,50 +16,34 @@ ...@@ -18,50 +16,34 @@
fileIcon, fileIcon,
changedFileIcon, changedFileIcon,
}, },
mixins: [
timeAgoMixin,
],
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: true,
}, },
showExtraColumns: { level: {
type: Boolean, type: Number,
default: false, required: true,
}, },
}, },
computed: { computed: {
...mapState([
'leftPanelCollapsed',
]),
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() { isTree() {
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
isBlob() {
return this.file.type === 'blob';
},
levelIndentation() { levelIndentation() {
if (this.file.level > 0) {
return { return {
marginLeft: `${this.file.level * 16}px`, marginLeft: `${this.level * 16}px`,
}; };
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
}, },
fileClass() { fileClass() {
if (this.file.type === 'blob') { return {
if (this.file.active) { 'file-open': this.isBlob && this.file.opened,
return 'file-open file-active'; 'file-active': this.isBlob && this.file.active,
} folder: this.isTree,
return this.file.opened ? 'file-open' : ''; };
} else if (this.file.type === 'tree') {
return 'folder';
}
return '';
}, },
}, },
updated() { updated() {
...@@ -70,27 +52,26 @@ ...@@ -70,27 +52,26 @@
} }
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
'updateDelayViewerUpdated', clickFile() {
]),
clickFile(row) {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) { if (
this.$store.dispatch('toggleTreeOpen', { this.isTree &&
endpoint: this.file.url, this.$router.currentRoute.path === `/project${this.file.url}`
tree: this.file, ) {
}); this.toggleTreeOpen(this.file.path);
} }
const delayPromise = this.file.changed ? const delayPromise = this.file.changed
Promise.resolve() : this.updateDelayViewerUpdated(true); ? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => { return delayPromise.then(() => {
this.$router.push(`/project${row.url}`); router.push(`/project${this.file.url}`);
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -101,54 +82,45 @@ ...@@ -101,54 +82,45 @@
> >
<div <div
class="file-name" class="file-name"
@click="clickFile(file)" @click="clickFile"
role="button"
> >
<a <span
class="ide-file-name str-truncated" class="ide-file-name str-truncated"
:style="levelIndentation"
> >
<file-icon <file-icon
:file-name="file.name" :file-name="file.name"
:loading="file.loading" :loading="file.loading"
:folder="file.type === 'tree'" :folder="isTree"
:opened="file.opened" :opened="file.opened"
:style="levelIndentation"
:size="16" :size="16"
/> />
{{ file.name }} {{ file.name }}
<file-status-icon :file="file" /> <file-status-icon
</a> :file="file"
/>
</span>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5 pull-right"
/>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree"
:project-id="file.projectId" :project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
:parent="file" class="pull-right prepend-left-8"
/> />
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5"
/>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</div> </div>
</div> </div>
<template <template v-if="file.opened">
v-if="file.opened"
>
<repo-file <repo-file
v-for="childFile in file.tree" v-for="childFile in file.tree"
:key="childFile.key" :key="childFile.key"
:file="childFile" :file="childFile"
:level="level + 1"
/> />
</template> </template>
</div> </div>
......
<script> <script>
import { mapGetters } from 'vuex';
export default { export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: { computed: {
...mapGetters([
'activeFile',
]),
showButtons() { showButtons() {
return this.activeFile.rawPath || return this.file.rawPath ||
this.activeFile.blamePath || this.file.blamePath ||
this.activeFile.commitsPath || this.file.commitsPath ||
this.activeFile.permalink; this.file.permalink;
}, },
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw'; return this.file.binary ? 'Download' : 'Raw';
}, },
}, },
}; };
...@@ -25,7 +26,7 @@ export default { ...@@ -25,7 +26,7 @@ export default {
class="multi-file-editor-btn-group" class="multi-file-editor-btn-group"
> >
<a <a
:href="activeFile.rawPath" :href="file.rawPath"
target="_blank" target="_blank"
class="btn btn-default btn-sm raw" class="btn btn-default btn-sm raw"
rel="noopener noreferrer"> rel="noopener noreferrer">
...@@ -38,19 +39,19 @@ export default { ...@@ -38,19 +39,19 @@ export default {
aria-label="File actions" aria-label="File actions"
> >
<a <a
:href="activeFile.blamePath" :href="file.blamePath"
class="btn btn-default btn-sm blame" class="btn btn-default btn-sm blame"
> >
Blame Blame
</a> </a>
<a <a
:href="activeFile.commitsPath" :href="file.commitsPath"
class="btn btn-default btn-sm history" class="btn btn-default btn-sm history"
> >
History History
</a> </a>
<a <a
:href="activeFile.permalink" :href="file.permalink"
class="btn btn-default btn-sm permalink" class="btn btn-default btn-sm permalink"
> >
Permalink Permalink
......
<script>
import $ from 'jquery';
import { mapGetters } from 'vuex';
import LineHighlighter from '~/line_highlighter';
import syntaxHighlight from '~/syntax_highlight';
export default {
computed: {
...mapGetters([
'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
},
},
mounted() {
this.highlightFile();
this.lineHighlighter = new LineHighlighter({
fileHolderSelector: '.blob-viewer-container',
scrollFileHolder: true,
});
},
updated() {
this.$nextTick(() => {
this.highlightFile();
});
},
methods: {
highlightFile() {
syntaxHighlight($(this.$el).find('.file-content'));
},
},
};
</script>
<template>
<div>
<div
v-if="!activeFile.renderError"
v-html="activeFile.html"
class="multi-file-preview-holder"
>
</div>
<div
v-else-if="activeFile.tempFile"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed for this temporary file.
</p>
</div>
<div
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large.
You can <a
:href="activeFile.rawPath"
download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred.
You can <a
:href="activeFile.rawPath"
download>download</a> it instead.
</p>
</div>
</div>
</template>
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
<button <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)" @click.stop.prevent="closeFile(tab.path)"
:aria-label="closeLabel" :aria-label="closeLabel"
> >
<icon <icon
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
...@@ -8,29 +8,33 @@ ...@@ -8,29 +8,33 @@
RepoTab, RepoTab,
EditorMode, EditorMode,
}, },
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
},
data() { data() {
return { return {
showShadow: false, showShadow: false,
}; };
}, },
computed: {
...mapGetters([
'hasChanges',
]),
...mapState([
'openFiles',
'viewer',
]),
},
updated() { updated() {
if (!this.$refs.tabsScroller) return; if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
}, },
methods: { methods: {
...mapActions([ ...mapActions(['updateViewer']),
'updateViewer',
]),
}, },
}; };
</script> </script>
...@@ -42,7 +46,7 @@ ...@@ -42,7 +46,7 @@
ref="tabsScroller" ref="tabsScroller"
> >
<repo-tab <repo-tab
v-for="tab in openFiles" v-for="tab in files"
:key="tab.key" :key="tab.key"
:tab="tab" :tab="tab"
/> />
......
<script>
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
components: {
PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
},
data() {
return {
width: this.initialWidth,
};
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
},
maxSize: (window.innerWidth / 2),
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed && collapsible,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
>
<slot></slot>
<panel-resizer
:size.sync="width"
:enabled="!collapsed"
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
@resize-start="setResizingStatus(true)"
@resize-end="setResizingStatus(false)"
:side="side === 'right' ? 'left' : 'right'"
/>
</div>
</template>
...@@ -2,9 +2,6 @@ import Vue from 'vue'; ...@@ -2,9 +2,6 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -76,7 +73,7 @@ router.beforeEach((to, from, next) => { ...@@ -76,7 +73,7 @@ router.beforeEach((to, from, next) => {
}) })
.then(() => { .then(() => {
if (to.params[0]) { if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]); const treeEntry = store.state.entries[to.params[0]];
if (treeEntry) { if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry); store.dispatch('handleTreeEntryAction', treeEntry);
} }
......
import Vue from 'vue'; import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
...@@ -8,11 +10,11 @@ export const setInitialData = ({ commit }, data) => ...@@ -8,11 +10,11 @@ export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data); commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => { export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => { state.changedFiles.forEach(file => {
commit(types.DISCARD_FILE_CHANGES, file); commit(types.DISCARD_FILE_CHANGES, file.path);
if (file.tempFile) { if (file.tempFile) {
dispatch('closeFile', file); dispatch('closeFile', file.path);
} }
}); });
...@@ -20,20 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => { ...@@ -20,20 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
}; };
export const closeAllFiles = ({ state, dispatch }) => { export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file)); state.openFiles.forEach(file => dispatch('closeFile', file.path));
};
export const toggleEditMode = ({ commit, dispatch }) => {
commit(types.TOGGLE_EDIT_MODE);
dispatch('toggleBlobView');
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
}; };
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
...@@ -49,28 +38,63 @@ export const setResizingStatus = ({ commit }, resizing) => { ...@@ -49,28 +38,63 @@ export const setResizingStatus = ({ commit }, resizing) => {
}; };
export const createTempEntry = ( export const createTempEntry = (
{ state, dispatch }, { state, commit, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false }, { branchId, name, type, content = '', base64 = false },
) => { ) =>
const selectedParent = parent || state.trees[`${projectId}/${branchId}`]; new Promise(resolve => {
if (type === 'tree') { const worker = new FilesDecoratorWorker();
dispatch('createTempTree', { const fullName =
projectId, name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
`The name "${name
.split('/')
.pop()}" is already taken in this directory.`,
'alert',
document,
null,
false,
true,
);
resolve();
return null;
}
worker.addEventListener('message', ({ data }) => {
const { file } = data;
worker.terminate();
commit(types.CREATE_TMP_ENTRY, {
data,
projectId: state.currentProjectId,
branchId, branchId,
parent: selectedParent,
name,
}); });
} else if (type === 'blob') {
dispatch('createTempFile', { if (type === 'blob') {
projectId, commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
}
resolve(file);
});
worker.postMessage({
data: [fullName],
projectId: state.currentProjectId,
branchId, branchId,
parent: selectedParent, type,
name, tempFile: true,
base64, base64,
content, content,
}); });
}
}; return null;
});
export const scrollToTab = () => { export const scrollToTab = () => {
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -95,4 +119,3 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -95,4 +119,3 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/branch';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.currentProjectId,
{
branch,
ref: state.currentBranchId,
},
)
.then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranchId, branchName);
if (this.$router) this.$router.push(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
...@@ -4,49 +4,44 @@ import eventHub from 'ee/ide/eventhub'; ...@@ -4,49 +4,44 @@ import eventHub from 'ee/ide/eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { import { setPageTitle } from '../utils';
findEntry,
setPageTitle, export const closeFile = ({ commit, state, getters, dispatch }, path) => {
createTemp, const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
findIndexOfFile, const file = state.entries[path];
} from '../utils';
export const closeFile = ({ commit, state, dispatch }, file) => {
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
const fileWasActive = file.active; const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { file, active: false }); commit(types.SET_FILE_ACTIVE, { path, active: false });
if (state.openFiles.length > 0 && fileWasActive) { if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen]; const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`); router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
} }
dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`); eventHub.$emit(`editor.update.model.dispose.${file.path}`);
}; };
export const setFileActive = ({ commit, state, getters, dispatch }, file) => { export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
const file = state.entries[path];
const currentActiveFile = getters.activeFile; const currentActiveFile = getters.activeFile;
if (file.active) return; if (file.active) return;
if (currentActiveFile) { if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false }); commit(types.SET_FILE_ACTIVE, {
path: currentActiveFile.path,
active: false,
});
} }
commit(types.SET_FILE_ACTIVE, { file, active: true }); commit(types.SET_FILE_ACTIVE, { path, active: true });
dispatch('scrollToTab'); dispatch('scrollToTab');
// reset hash for line highlighting
location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId); commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId); commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
...@@ -54,104 +49,97 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => { ...@@ -54,104 +49,97 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
service.getFileData(file.url) return service
.then((res) => { .getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle); setPageTitle(pageTitle);
return res.json(); return res.json();
}) })
.then((data) => { .then(data => {
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file); dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
flash('Error loading file data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
}); });
}; };
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file) export const getRawFileData = ({ commit, dispatch }, file) =>
.then((raw) => { service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); commit(types.SET_FILE_RAW_DATA, { file, raw });
}) })
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true)); .catch(() =>
flash(
export const changeFileContent = ({ state, commit }, { file, content }) => { 'Error loading file content. Please try again.',
commit(types.UPDATE_FILE_CONTENT, { file, content }); 'alert',
document,
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file); null,
false,
true,
),
);
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) { if (file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, file); commit(types.ADD_FILE_TO_CHANGED, path);
} else if (!file.changed && indexOfChangedFile !== -1) { } else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, file); commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
} }
}; };
export const setFileEOL = ({ state, commit }, { eol }) => { export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (state.selectedFile) { if (getters.activeFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
} }
}; };
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { export const setFileEOL = ({ getters, commit }, { eol }) => {
if (state.selectedFile) { if (getters.activeFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
} }
}; };
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { export const setEditorPosition = (
const path = parent.path !== undefined ? parent.path : ''; { getters, commit },
// We need to do the replacement otherwise the web_url + file.url duplicate { editorRow, editorColumn },
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`; ) => {
const file = createTemp({ if (getters.activeFile) {
projectId, commit(types.SET_FILE_POSITION, {
branchId, file: getters.activeFile,
name: name.replace(`${path}/`, ''), editorRow,
path, editorColumn,
type: 'blob',
level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
url: newUrl,
});
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
file,
}); });
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.ADD_FILE_TO_CHANGED, file);
dispatch('setFileActive', file);
if (!state.editMode && !file.base64) {
dispatch('toggleEditMode', true);
} }
router.push(`/project${file.url}`);
return Promise.resolve(file);
}; };
export const discardFileChanges = ({ commit }, file) => { export const discardFileChanges = ({ state, commit }, path) => {
commit(types.DISCARD_FILE_CHANGES, file); const file = state.entries[path];
commit(types.REMOVE_FILE_FROM_CHANGED, file);
commit(types.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
if (file.tempFile && file.opened) { if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, path);
} }
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
......
...@@ -2,7 +2,6 @@ import flash from '~/flash'; ...@@ -2,7 +2,6 @@ import flash from '~/flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = ( export const getProjectData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ namespace, projectId, force = false } = {}, { namespace, projectId, force = false } = {},
...@@ -25,3 +24,26 @@ export const getProjectData = ( ...@@ -25,3 +24,26 @@ export const getProjectData = (
resolve(state.projects[`${namespace}/${projectId}`]); resolve(state.projects[`${namespace}/${projectId}`]);
} }
}); });
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
import { visitUrl } from '~/lib/utils/url_utility';
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router';
import { import {
setPageTitle,
findEntry, findEntry,
createTemp,
createOrMergeEntry,
sortTree,
} from '../utils'; } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const getTreeData = ( export const toggleTreeOpen = ({ commit, dispatch }, path) => {
{ commit, state, dispatch }, commit(types.TOGGLE_TREE_OPEN, path);
{ endpoint, tree = null, projectId, branch, force = false } = {},
) => new Promise((resolve, reject) => {
// We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
resolve();
} else {
if (tree) commit(types.TOGGLE_LOADING, { entry: tree });
const selectedProject = state.projects[projectId];
// We are merging the web_url that we got on the project info with the endpoint
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
if (completeEndpoint && (!tree || !tree.tempFile)) {
service.getTreeData(completeEndpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
dispatch('updateDirectoryData', { data, tree, projectId, branch, clearTree: false });
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
if (tree) commit(types.TOGGLE_LOADING, { entry: selectedTree });
const prevLastCommitPath = selectedTree.lastCommitPath;
if (prevLastCommitPath !== null) {
dispatch('getLastCommitData', selectedTree);
}
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, { entry: tree });
reject(e);
});
} else {
resolve();
}
}
});
export const toggleTreeOpen = ({ commit, dispatch }, { tree }) => {
commit(types.TOGGLE_TREE_OPEN, tree);
}; };
export const handleTreeEntryAction = ({ commit, dispatch }, row) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') { if (row.type === 'tree') {
dispatch('toggleTreeOpen', { dispatch('toggleTreeOpen', row.path);
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, { entry: row });
visitUrl(row.url);
} else if (row.type === 'blob' && (row.opened || row.changed)) { } else if (row.type === 'blob' && (row.opened || row.changed)) {
if (row.changed && !row.opened) { if (row.changed && !row.opened) {
commit(types.TOGGLE_FILE_OPEN, row); commit(types.TOGGLE_FILE_OPEN, row.path);
} }
dispatch('setFileActive', row); dispatch('setFileActive', row.path);
} else { } else {
dispatch('getFileData', row); dispatch('getFileData', row);
} }
}; };
export const createTempTree = (
{ state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => {
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({
projectId,
branchId,
name: dirName,
path,
type: 'tree',
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
});
commit(types.CREATE_TMP_TREE, {
parent: selectedTree,
tmpEntry,
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else {
selectedTree = foundEntry;
}
});
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
...@@ -146,47 +50,6 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -146,47 +50,6 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const updateDirectoryData = (
{ commit, state },
{ data, tree, projectId, branch, clearTree = true },
) => {
if (!tree) {
const existingTree = state.trees[`${projectId}/${branch}`];
if (!existingTree) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
}
}
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({
projectId: `${projectId}`,
branchId: branch,
entry,
level,
type,
parentTreeUrl,
state,
});
let formattedData = [
...data.trees.map(t => createEntry(t, 'tree')),
...data.submodules.map(m => createEntry(m, 'submodule')),
...data.blobs.map(b => createEntry(b, 'blob')),
];
if (!clearTree && tree) {
const tempFiles = state.changedFiles.filter(f => f.tempFile && f.path === `${tree.path}/${f.name}`);
if (tempFiles.length) {
formattedData = formattedData.concat(tempFiles);
}
}
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
};
export const getFiles = ( export const getFiles = (
{ state, commit, dispatch }, { state, commit, dispatch },
{ projectId, branchId } = {}, { projectId, branchId } = {},
...@@ -199,75 +62,25 @@ export const getFiles = ( ...@@ -199,75 +62,25 @@ export const getFiles = (
.getFiles(selectedProject.web_url, branchId) .getFiles(selectedProject.web_url, branchId)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
const newTree = data.reduce((outputArray, file) => { const worker = new FilesDecoratorWorker();
const pathSplit = file.split('/'); worker.addEventListener('message', (e) => {
const blobName = pathSplit.pop(); const { entries, treeList } = e.data;
let selectedFolderTree; const selectedTree = state.trees[`${projectId}/${branchId}`];
let foundFolder = null;
let fullPath = '';
if (pathSplit.length > 0) { commit(types.SET_ENTRIES, entries);
const newBaseFolders = pathSplit.reduce((newFolders, folder, currentIndex) => { commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
fullPath += `/${folder}`; commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
foundFolder = findEntry(selectedFolderTree || outputArray, 'tree', fullPath, 'path');
if (!foundFolder) {
foundFolder = createOrMergeEntry({
projectId,
branchId,
entry: {
id: fullPath,
name: folder,
path: fullPath,
url: `/${projectId}/tree/${branchId}/${fullPath}`,
},
level: currentIndex,
type: 'tree',
parentTreeUrl: '',
state,
});
if (selectedFolderTree) { worker.terminate();
selectedFolderTree.push(foundFolder);
} else { resolve();
newFolders.push(foundFolder); });
}
}
selectedFolderTree = foundFolder.tree;
return newFolders;
}, []);
if (newBaseFolders.length) outputArray.push(newBaseFolders[0]);
}
// Add file worker.postMessage({
const blobEntry = createOrMergeEntry({ data,
projectId, projectId,
branchId, branchId,
entry: {
id: file,
name: blobName,
path: file,
url: `/${projectId}/blob/${branchId}/${file}`,
},
level: foundFolder ? (foundFolder.level + 1) : 0,
type: 'blob',
parentTreeUrl: foundFolder ? foundFolder.url : '',
state,
}); });
if (selectedFolderTree) {
selectedFolderTree.push(blobEntry);
} else {
outputArray.push(blobEntry);
}
return outputArray;
}, []);
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: sortTree(newTree) });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
resolve();
}) })
.catch((e) => { .catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
......
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state =>
state.openFiles.find(file => file.active) || null;
export const activeFileExtension = (state) => { export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const canEditFile = (state) => { export const modifiedFiles = state =>
const currentActiveFile = activeFile(state); state.changedFiles.filter(f => !f.tempFile);
return state.canCommit && export const projectsWithTrees = state =>
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary); Object.keys(state.projects).map(projectId => {
}; const project = state.projects[projectId];
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); return {
...project,
branches: Object.keys(project.branches).map(branchId => {
const branch = project.branches[branchId];
return {
...branch,
tree: state.trees[branch.treeId],
};
}),
};
});
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile); // eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length; export const hasChanges = state => !!state.changedFiles.length;
...@@ -137,6 +137,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -137,6 +137,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
} }
dispatch('setLastCommitMessage', data); dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch( dispatch(
......
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING'; export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
...@@ -20,7 +19,6 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; ...@@ -20,7 +19,6 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types // Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE'; export const CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES'; export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
...@@ -35,17 +33,11 @@ export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; ...@@ -35,17 +33,11 @@ export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION'; export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_EOL = 'SET_FILE_EOL'; export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED'; export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
// Viewer mutation types
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
...@@ -8,25 +8,19 @@ export default { ...@@ -8,25 +8,19 @@ export default {
[types.SET_INITIAL_DATA](state, data) { [types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data); Object.assign(state, data);
}, },
[types.SET_PREVIEW_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-preview',
});
},
[types.SET_EDIT_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-editor',
});
},
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, { Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading, loading: forceValue !== undefined ? forceValue : !entry.loading,
}); });
}, }
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
}, },
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) { [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, { Object.assign(state, {
...@@ -57,6 +51,44 @@ export default { ...@@ -57,6 +51,44 @@ export default {
lastCommitMsg, lastCommitMsg,
}); });
}, },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
});
},
[types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
Object.keys(data.entries).reduce((acc, key) => {
const entry = data.entries[key];
const foundEntry = state.entries[key];
if (!foundEntry) {
Object.assign(state.entries, {
[key]: entry,
});
} else {
const tree = entry.tree.filter(
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree),
});
}
return acc.concat(key);
}, []);
const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
e => e.path === data.treeList[0].path,
);
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
});
}
},
[types.UPDATE_VIEWER](state, viewer) { [types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, { Object.assign(state, {
viewer, viewer,
......
...@@ -7,16 +7,14 @@ export default { ...@@ -7,16 +7,14 @@ export default {
}); });
}, },
[types.SET_BRANCH](state, { projectPath, branchName, branch }) { [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties Object.assign(state.projects[projectPath], {
Object.assign(branch, { branches: {
[branchName]: {
...branch,
treeId: `${projectPath}/${branchName}`, treeId: `${projectPath}/${branchName}`,
active: true, active: true,
workingReference: '', workingReference: '',
}); },
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
}, },
}); });
}, },
......
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default { export default {
[types.SET_FILE_ACTIVE](state, { file, active }) { [types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(file, { Object.assign(state.entries[path], {
active, active,
}); });
Object.assign(state, {
selectedFile: file,
});
}, },
[types.TOGGLE_FILE_OPEN](state, file) { [types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(file, { Object.assign(state.entries[path], {
opened: !file.opened, opened: !state.entries[path].opened,
}); });
if (file.opened) { if (state.entries[path].opened) {
state.openFiles.push(file); state.openFiles.push(state.entries[path]);
} else { } else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1); Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path),
});
} }
}, },
[types.SET_FILE_DATA](state, { data, file }) { [types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
id: data.id, id: data.id,
blamePath: data.blame_path, blamePath: data.blame_path,
commitsPath: data.commits_path, commitsPath: data.commits_path,
...@@ -34,53 +31,52 @@ export default { ...@@ -34,53 +31,52 @@ export default {
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
raw, raw,
}); });
}, },
[types.UPDATE_FILE_CONTENT](state, { file, content }) { [types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== file.raw; const changed = content !== state.entries[path].raw;
Object.assign(file, { Object.assign(state.entries[path], {
content, content,
changed, changed,
}); });
}, },
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) { [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
fileLanguage, fileLanguage,
}); });
}, },
[types.SET_FILE_EOL](state, { file, eol }) { [types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
eol, eol,
}); });
}, },
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) { [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
editorRow, editorRow,
editorColumn, editorColumn,
}); });
}, },
[types.DISCARD_FILE_CHANGES](state, file) { [types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(file, { Object.assign(state.entries[path], {
content: file.raw, content: state.entries[path].raw,
changed: false, changed: false,
}); });
}, },
[types.CREATE_TMP_FILE](state, { file, parent }) { [types.ADD_FILE_TO_CHANGED](state, path) {
parent.tree.push(file); Object.assign(state, {
}, changedFiles: state.changedFiles.concat(state.entries[path]),
[types.ADD_FILE_TO_CHANGED](state, file) { });
state.changedFiles.push(file);
}, },
[types.REMOVE_FILE_FROM_CHANGED](state, file) { [types.REMOVE_FILE_FROM_CHANGED](state, path) {
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file); Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
state.changedFiles.splice(indexOfChangedFile, 1); });
}, },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) { [types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, { Object.assign(state.entries[file.path], {
changed, changed,
}); });
}, },
......
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export default { export default {
[types.TOGGLE_TREE_OPEN](state, tree) { [types.TOGGLE_TREE_OPEN](state, path) {
Object.assign(tree, { Object.assign(state.entries[path], {
opened: !tree.opened, opened: !state.entries[path].opened,
}); });
}, },
[types.CREATE_TREE](state, { treePath }) { [types.CREATE_TREE](state, { treePath }) {
...@@ -16,14 +16,13 @@ export default { ...@@ -16,14 +16,13 @@ export default {
}), }),
}); });
}, },
[types.SET_DIRECTORY_DATA](state, { data, tree }) { [types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(tree, { Object.assign(state, {
trees: Object.assign(state.trees, {
[treePath]: {
tree: data, tree: data,
});
}, },
[types.SET_PARENT_TREE_URL](state, url) { }),
Object.assign(state, {
parentTreeUrl: url,
}); });
}, },
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
...@@ -31,9 +30,6 @@ export default { ...@@ -31,9 +30,6 @@ export default {
lastCommitPath: url, lastCommitPath: url,
}); });
}, },
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
[types.REMOVE_ALL_CHANGES_FILES](state) { [types.REMOVE_ALL_CHANGES_FILES](state) {
Object.assign(state, { Object.assign(state, {
changedFiles: [], changedFiles: [],
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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