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'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
......@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
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-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......@@ -116,7 +116,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
gem 'google-api-client', '~> 0.19'
gem 'google-api-client', '~> 0.19.8'
# for aws storage
gem 'unf', '~> 0.1.4'
......@@ -245,9 +245,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
# Faster JSON
gem 'oj', '~> 2.17.4'
# Faster blank
gem 'fast_blank'
......@@ -291,7 +288,6 @@ gem 'batch-loader', '~> 1.2.1'
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
......
......@@ -47,6 +47,7 @@ GEM
memoizable (~> 0.4.0)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
......@@ -94,7 +95,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.4.1)
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
......@@ -184,10 +185,10 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6)
doorkeeper (4.3.1)
railties (>= 4.2)
doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
......@@ -457,10 +458,10 @@ GEM
jmespath (1.3.1)
jquery-atwho-rails (1.3.2)
json (1.8.6)
json-jwt (1.7.2)
json-jwt (1.9.2)
activesupport
aes_key_wrap
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.8.0)
......@@ -553,7 +554,6 @@ GEM
rack (>= 1.2, < 3)
octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.5)
omniauth (1.4.3)
hashie (>= 1.2, < 4)
rack (>= 1.6.2, < 3)
......@@ -623,8 +623,6 @@ GEM
railties (>= 4.0.0)
peek-gc (0.0.2)
peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0)
atomic (>= 1.0.0)
mysql2
......@@ -707,8 +705,8 @@ GEM
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0)
rails-dom-testing (1.0.9)
activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
......@@ -1061,8 +1059,8 @@ DEPENDENCIES
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.2.0)
doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1)
elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9)
......@@ -1103,7 +1101,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
......@@ -1144,7 +1142,6 @@ DEPENDENCIES
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
......@@ -1156,14 +1153,13 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10.0)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
......
import './autosize';
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 './details_behavior';
import installGlEmojiElement from './gl_emoji';
......
......@@ -2,8 +2,8 @@
import $ from 'jquery';
import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
import { placeholderImage } from '../lazy_loader';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
import { placeholderImage } from '~/lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown
//
......
import $ from 'jquery';
import { __ } from './locale';
import flash from './flash';
import { __ } from '~/locale';
import flash from '~/flash';
// Renders math using KaTeX in any element with the
// `js-render-math` class
......
import flash from '~/flash';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
//
......@@ -12,8 +14,6 @@
// </pre>
//
import Flash from './flash';
export default function renderMermaid($els) {
if (!$els.length) return;
......@@ -52,6 +52,6 @@ export default function renderMermaid($els) {
});
});
}).catch((err) => {
Flash(`Can't load mermaid module: ${err}`);
flash(`Can't load mermaid module: ${err}`);
});
}
......@@ -53,8 +53,12 @@ function initPageShortcuts(page) {
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
const gfm = new GfmAutoComplete(
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
);
const enableGFM = convertPermissionToBoolean(
el.dataset.supportsAutocomplete,
);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
......@@ -67,9 +71,9 @@ function initGFMInput() {
}
function initPerformanceBar() {
if (document.querySelector('#peek')) {
if (document.querySelector('#js-peek')) {
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'));
}
}
......
......@@ -32,7 +32,6 @@ import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import './render_gfm';
import initBreadcrumbs from './breadcrumb';
// EE-only scripts
......
......@@ -73,6 +73,10 @@
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
......@@ -188,6 +192,7 @@
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
/>
</template>
......@@ -27,6 +27,10 @@
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
......@@ -54,7 +58,7 @@
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
svgUrl: this.emptyNoDataSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
......
......@@ -105,6 +105,9 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
......@@ -138,10 +141,6 @@ export default class Notes {
}
addBinding() {
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
......@@ -226,14 +225,9 @@ export default class Notes {
$(window).on('hashchange', this.onHashChange);
this.boundGetContent = this.getContent.bind(this);
document.addEventListener('refreshLegacyNotes', this.boundGetContent);
this.eventsBound = true;
}
cleanBinding() {
if (!this.eventsBound) {
return;
}
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
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';
import _ from 'underscore';
import Sidebar from './right_sidebar';
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 {
constructor(isMergeRequest) {
......
.navbar-gitlab {
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
border-radius: 0;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.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;
}
}
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
border-radius: 0;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
......@@ -184,6 +148,38 @@
}
.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 {
@media (max-width: $screen-xs-max) {
display: -webkit-flex;
......
......@@ -140,12 +140,6 @@ ul.notes {
@include bulleted-list;
word-wrap: break-word;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
table {
@include markdown-table;
}
......
......@@ -68,7 +68,6 @@
.ide-new-btn {
display: none;
margin-top: -4px;
margin-bottom: -4px;
margin-right: -8px;
}
......@@ -84,7 +83,6 @@
fill: $gl-text-color-secondary;
}
}
}
a {
......@@ -290,7 +288,7 @@
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
opacity: 0.4;
}
}
}
......@@ -548,7 +546,6 @@
height: 10px;
margin-left: 3px;
}
}
.multi-file-commit-list-path {
......@@ -626,7 +623,7 @@
top: 0;
width: 100px;
height: 1px;
background-color: rgba($red-500, .5);
background-color: rgba($red-500, 0.5);
}
}
}
......@@ -720,12 +717,13 @@
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
height: calc(
100vh - #{$header-height + $performance-bar-height + $flash-height}
);
}
}
}
.dragHandle {
position: absolute;
top: 0;
......
@import "framework/variables";
@import "peek/views/performance_bar";
@import "peek/views/rblineprof";
@import 'framework/variables';
@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof';
#peek {
#js-peek {
position: fixed;
left: 0;
top: 0;
......@@ -21,14 +21,26 @@
&.production {
background-color: $perf-bar-production;
select {
background: $perf-bar-production;
}
}
&.staging {
background-color: $perf-bar-staging;
select {
background: $perf-bar-staging;
}
}
&.development {
background-color: $perf-bar-development;
select {
background: $perf-bar-development;
}
}
.wrapper {
......@@ -42,11 +54,12 @@
background: $perf-bar-bucket-bg;
display: inline-block;
padding: 4px 6px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
color: $perf-bar-bucket-color;
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 {
display: none;
......@@ -94,6 +107,10 @@
max-width: 10000px !important;
}
}
.performance-bar-modal .modal-footer {
display: none;
}
}
#modal-peek-pg-queries-content {
......
......@@ -16,6 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items)
items = by_personal(items)
items = by_name(items)
items = items.includes(namespace: [:owner])
sort(items).page(params[:page])
end
......
......@@ -304,7 +304,7 @@ module ApplicationHelper
def linkedin_url(user)
name = user.linkedin
if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/}
if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/}
name
else
"https://www.linkedin.com/in/#{name}"
......@@ -313,10 +313,10 @@ module ApplicationHelper
def twitter_url(user)
name = user.twitter
if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/}
if name =~ %r{\Ahttps?://(www\.)?twitter\.com/}
name
else
"https://www.twitter.com/#{name}"
"https://twitter.com/#{name}"
end
end
......
module 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)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
......
......@@ -35,7 +35,8 @@ class NotificationRecipient
# check this last because it's expensive
# 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
end
......
......@@ -14,9 +14,8 @@ class JiraService < IssueTrackerService
alias_method :project_url, :url
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
# When these are false GitLab does not create cross reference
# comments on JIRA except when an issue gets transitioned.
def self.supported_events
%w(commit merge_request)
end
......@@ -318,4 +317,13 @@ class JiraService < IssueTrackerService
url_changed?
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
......@@ -309,6 +309,29 @@ class Service < ActiveRecord::Base
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?
activated? && !importing?
end
......
......@@ -93,6 +93,7 @@ module Projects
# hook failed and caused us to end up here. A destroyed model will be a frozen hash,
# which cannot be altered.
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}")
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 @@
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.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'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
......
......@@ -33,7 +33,7 @@
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.light
= service_event_description(event)
= @service.class.event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
......
......@@ -62,7 +62,7 @@
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%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]" } }
%span.dropdown-toggle-text
= member.human_access
......@@ -82,7 +82,7 @@
can_override: member.can_override?
.prepend-left-5.clearable-input.member-form-control
= 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',
placeholder: 'Expiration date',
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
# This patch fixes https://github.com/rails/rails/issues/26024
# TODO: Remove it when it's no longer necessary
module ActiveRecord
module Locking
module Optimistic
# We overwrite this method because we don't want to have default value
# for newly created records
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
super
end
# Remove this entire initializer when we are at rails 5.0.
# 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 Locking
module Optimistic
# We overwrite this method because we don't want to have default value
# for newly created records
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
super
end
def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
lock_col = self.class.locking_column
lock_col = self.class.locking_column
previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
increment_lock
increment_lock
attribute_names += [lock_col]
attribute_names.uniq!
attribute_names += [lock_col]
attribute_names.uniq!
begin
relation = self.class.unscoped
begin
relation = self.class.unscoped
affected_rows = relation.where(
self.class.primary_key => id,
lock_col => previous_lock_value
).update_all(
attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
end.to_h
)
affected_rows = relation.where(
self.class.primary_key => id,
lock_col => previous_lock_value
).update_all(
attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
end.to_h
)
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
end
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
end
affected_rows
affected_rows
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise
end
end
end
# This is patched because we need it to query `lock_version IS NULL`
# rather than `lock_version = 0` whenever lock_version is NULL.
def relation_for_destroy
return super unless locking_enabled?
# This is patched because we need it to query `lock_version IS NULL`
# rather than `lock_version = 0` whenever lock_version is NULL.
def relation_for_destroy
return super unless locking_enabled?
column_name = self.class.locking_column
super.where(self.class.arel_table[column_name].eq(self[column_name]))
column_name = self.class.locking_column
super.where(self.class.arel_table[column_name].eq(self[column_name]))
end
end
end
# This is patched because we want `lock_version` default to `NULL`
# rather than `0`
class LockingType < SimpleDelegator
def type_cast_from_database(value)
super
# This is patched because we want `lock_version` default to `NULL`
# rather than `0`
class LockingType < SimpleDelegator
def type_cast_from_database(value)
super
end
end
end
end
......
......@@ -59,7 +59,7 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check'
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
mount Peek::Railtie => '/peek', as: 'peek_routes'
# Boards resources shared between group and projects
resources :boards, only: [] do
......
......@@ -11,7 +11,7 @@
#
# 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
enable_extension "plpgsql"
......@@ -184,6 +184,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.string "external_authorization_service_url"
t.string "external_authorization_service_default_label"
t.boolean "pages_domain_verification_enabled", default: true, null: false
t.float "external_authorization_service_timeout", default: 0.5, null: false
end
create_table "approvals", force: :cascade do |t|
......
......@@ -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)
- 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)
- 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)
- time taken and number of calls to Redis
- time taken and number of background jobs created by Sidekiq
- 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
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`
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 |
......@@ -19,7 +19,7 @@ GET /groups/:id/-/epics/:epic_iid/issues
| `epic_iid` | integer/string | yes | The internal ID of the epic. |
```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:
......@@ -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.
```
POST /groups/:id/-/epics/:epic_iid/issues/:issue_id
POST /groups/:id/epics/:epic_iid/issues/:issue_id
```
| Attribute | Type | Required | Description |
......@@ -116,7 +116,7 @@ POST /groups/:id/-/epics/:epic_iid/issues/:issue_id
| `issue_id` | integer/string | yes | The ID of the issue. |
```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:
......@@ -212,7 +212,7 @@ Example response:
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 |
......@@ -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. |
```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:
......@@ -318,7 +318,7 @@ Example response:
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 |
......@@ -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. |
```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:
......
......@@ -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.
```
GET /groups/:id/-/epics
GET /groups/:id/-/epics?author_id=5
GET /groups/:id/-/epics?labels=bug,reproduced
GET /groups/:id/epics
GET /groups/:id/epics?author_id=5
GET /groups/:id/epics?labels=bug,reproduced
```
| Attribute | Type | Required | Description |
......@@ -30,7 +30,7 @@ GET /groups/:id/-/epics?labels=bug,reproduced
| `search` | string | no | Search epics against their `title` and `description` |
```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:
......@@ -65,7 +65,7 @@ Example response:
Gets a single epic
```
GET /groups/:id/-/epics/:epic_iid
GET /groups/:id/epics/:epic_iid
```
| Attribute | Type | Required | Description |
......@@ -74,7 +74,7 @@ GET /groups/:id/-/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic. |
```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:
......@@ -106,7 +106,7 @@ Example response:
Creates a new epic
```
POST /groups/:id/-/epics
POST /groups/:id/epics
```
| Attribute | Type | Required | Description |
......@@ -119,7 +119,7 @@ POST /groups/:id/-/epics
| `end_date` | string. | no | The end date of the epic |
```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:
......@@ -152,7 +152,7 @@ Example response:
Updates an epic
```
PUT /groups/:id/-/epics/:epic_iid
PUT /groups/:id/epics/:epic_iid
```
| Attribute | Type | Required | Description |
......@@ -166,7 +166,7 @@ PUT /groups/:id/-/epics/:epic_iid
| `end_date` | string. | no | The end date of an epic |
```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:
......@@ -199,7 +199,7 @@ Example response:
Deletes an epic
```
DELETE /groups/:id/-/epics/:epic_iid
DELETE /groups/:id/epics/:epic_iid
```
| Attribute | Type | Required | Description |
......@@ -208,5 +208,5 @@ DELETE /groups/:id/-/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic. |
```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.
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 |
......@@ -392,7 +392,7 @@ The response depends on the requested scope.
### Scope: projects
```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:
......@@ -423,7 +423,7 @@ Example response:
### Scope: issues
```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:
......@@ -488,7 +488,7 @@ Example response:
### Scope: merge_requests
```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:
......@@ -565,7 +565,7 @@ Example response:
### Scope: milestones
```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:
......@@ -592,7 +592,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```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:
......@@ -617,7 +617,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```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:
......@@ -650,7 +650,7 @@ Example response:
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
```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:
......@@ -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.
```
GET /projects/:id/-/search
GET /projects/:id/search
```
| Attribute | Type | Required | Description |
......@@ -694,7 +694,7 @@ The response depends on the requested scope.
### Scope: issues
```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:
......@@ -759,7 +759,7 @@ Example response:
### Scope: merge_requests
```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:
......@@ -836,7 +836,7 @@ Example response:
### Scope: milestones
```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:
......@@ -861,7 +861,7 @@ Example response:
### Scope: notes
```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:
......@@ -893,7 +893,7 @@ Example response:
### Scope: wiki_blobs
```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:
......@@ -916,7 +916,7 @@ Example response:
### Scope: commits
```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:
......@@ -947,7 +947,7 @@ Example response:
### Scope: blobs
```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:
......
......@@ -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_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_timeout` | float | no | The timeout to enforce when performing requests to the external authorization service |
```bash
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:
"polling_interval_multiplier": 1.0,
"external_authorization_service_enabled": true,
"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
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
......
......@@ -38,6 +38,9 @@ admin area under the settings page:
The available required properties are:
- **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
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*
The default cluster configuration grants access to a wide set of
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.
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
In order to publish your web application, you first need to find the external IP
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
**Advanced settings**, or go directly to the
[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
be used to set up DNS entries and forwarding rules that allow external access to
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
NOTE: **Note:**
......@@ -279,6 +308,14 @@ GitLab CI/CD build environment.
| `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. |
## 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
After you have successfully added your cluster information, you can enable the
......
......@@ -2,7 +2,7 @@
> [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
within the GitLab interface.
......@@ -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 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
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)
* 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.
......@@ -32,7 +36,7 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma
![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/).
......@@ -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.
## Manual configuration of Prometheus
### Manual configuration of Prometheus
### Requirements
#### Requirements
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. GitLab must have network connectivity to the Prometheus server
### Getting started
#### Getting started
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
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)
### Configuration in GitLab
#### Configuration in GitLab
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
......@@ -83,9 +87,33 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
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
......@@ -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.
> 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
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
......@@ -109,7 +137,7 @@ Prometheus server.
## 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.
- Prometheus does not have performance data for this environment, or the metrics
......
......@@ -52,7 +52,7 @@
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file)"
@click="discardFileChanges(file.path)"
>
Discard
</button>
......
......@@ -5,7 +5,6 @@
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
......@@ -16,7 +15,6 @@
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
props: {
emptyStateSvgPath: {
......@@ -33,18 +31,12 @@
},
},
computed: {
...mapState([
'currentBlobView',
'selectedFile',
'changedFiles',
]),
...mapGetters([
'activeFile',
]),
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
......@@ -67,20 +59,29 @@
<template
v-if="activeFile"
>
<repo-tabs/>
<component
<repo-tabs
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/>
<repo-editor
class="multi-file-edit-pane-content"
:is="currentBlobView"
:file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/>
<repo-file-buttons />
<ide-status-bar
:file="selectedFile"
:file="activeFile"
/>
</template>
<template
v-else
>
<div class="ide-empty-state">
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
......
<script>
import { mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
data() {
return {
width: 340,
};
},
computed: {
...mapState([
'rightPanelCollapsed',
'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: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
......@@ -104,7 +64,10 @@
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="toggleCollapsed"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
......@@ -117,15 +80,5 @@
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="340"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
export default {
components: {
repoTree,
icon,
newDropdown,
},
branch: {
type: Object,
required: true,
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
},
};
};
</script>
<template>
......@@ -40,8 +40,8 @@ export default {
/>
</div>
</div>
<div>
<repo-tree :tree-id="branch.treeId" />
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import repoFile from './repo_file.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
export default {
components: {
repoFile,
skeletonLoadingContainer,
RepoFile,
SkeletonLoadingContainer,
},
props: {
treeId: {
type: String,
tree: {
type: Object,
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>
<template>
<div
class="ide-file-list"
v-if="treeId"
>
<template v-if="showLoading">
<template v-if="tree.loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -47,10 +29,13 @@ export default {
<skeleton-loading-container />
</div>
</template>
<repo-file
v-for="file in selctedTree"
:key="file.key"
:file="file"
/>
<template v-else>
<repo-file
v-for="file in tree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</template>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
......@@ -11,65 +12,27 @@
icon,
panelResizer,
skeletonLoadingContainer,
},
data() {
return {
width: 290,
};
ResizablePanel,
},
computed: {
...mapState([
'loading',
'projects',
'leftPanelCollapsed',
]),
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
showLoading() {
return this.loading;
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
...mapGetters([
'projectsWithTrees',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': leftPanelCollapsed,
}"
:style="panelStyle"
<resizable-panel
:collapsible="false"
:initial-width="290"
side="left"
>
<div class="multi-file-commit-panel-inner">
<template v-if="showLoading">
<template v-if="loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -79,36 +42,10 @@
</div>
</template>
<project-tree
v-for="project in projects"
v-for="project in projectsWithTrees"
:key="project.id"
:project="project"
/>
</div>
<button
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>
</resizable-panel>
</template>
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
......@@ -20,11 +19,6 @@
required: true,
},
},
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
......@@ -35,32 +29,32 @@
name="branch"
:size="12"
/>
{{ selectedFile.branchId }}
{{ file.branchId }}
</div>
<div>
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url"
:title="file.lastCommit.message"
:href="file.lastCommit.url"
>
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
{{ timeFormated(file.lastCommit.updatedAt) }} by
{{ file.lastCommit.author }}
</a>
</div>
</div>
<div class="text-right">
{{ selectedFile.name }}
{{ file.name }}
</div>
<div class="text-right">
{{ selectedFile.eol }}
{{ file.eol }}
</div>
<div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
{{ selectedFile.fileLanguage }}
{{ file.fileLanguage }}
</div>
</div>
</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>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
......@@ -18,10 +19,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
data() {
return {
......@@ -31,6 +28,9 @@
};
},
methods: {
...mapActions([
'createTempEntry',
]),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
......@@ -85,7 +85,7 @@
<upload
:branch-id="branch"
:path="path"
:parent="parent"
@create="createTempEntry"
/>
</li>
<li>
......@@ -104,8 +104,8 @@
:type="modalType"
:branch-id="branch"
:path="path"
:parent="parent"
@hide="hideModal"
@create="createTempEntry"
/>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import modal from '~/vue_shared/components/modal.vue';
......@@ -12,10 +11,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: {
type: String,
required: true,
......@@ -31,9 +26,6 @@
};
},
computed: {
...mapState([
'currentProjectId',
]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
......@@ -60,15 +52,10 @@
this.$refs.fieldName.focus();
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
this.$emit('create', {
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
name: this.entryName,
type: this.type,
});
......
<script>
import { mapActions, mapState } from 'vuex';
export default {
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
path: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
......@@ -25,9 +18,6 @@
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
......@@ -36,11 +26,9 @@
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
projectId: this.currentProjectId,
this.$emit('create', {
name: `${(this.path ? `${this.path}/` : '')}${name}`,
branchId: this.branchId,
parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
......
......@@ -53,7 +53,6 @@ export default {
},
methods: {
...mapActions([
'getTreeData',
'setPanelCollapsedStatus',
]),
...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>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
return this.file && this.file.binary && !this.file.raw;
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
file(oldVal, newVal) {
if (newVal.path !== this.file.path) {
this.initMonaco();
}
},
......@@ -34,11 +35,6 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
viewer() {
this.createEditorInstance();
},
......@@ -72,7 +68,7 @@ export default {
this.editor.clearEditor();
this.getRawFileData(this.activeFile)
this.getRawFileData(this.file)
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
......@@ -101,9 +97,9 @@ export default {
});
},
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);
......@@ -112,7 +108,7 @@ export default {
if (file.active) {
this.changeFileContent({
file,
path: file.path,
content: model.getModel().getValue(),
});
}
......@@ -127,8 +123,8 @@ export default {
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
lineNumber: this.file.editorRow,
column: this.file.editorColumn,
});
// Handle File Language
......@@ -152,7 +148,7 @@ export default {
>
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
v-html="file.html"
>
</div>
<div
......
<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';
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',
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
export default {
name: 'RepoFile',
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
},
props: {
file: {
type: Object,
required: true,
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
showExtraColumns: {
type: Boolean,
default: false,
},
level: {
type: Number,
required: true,
},
computed: {
...mapState([
'leftPanelCollapsed',
]),
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() {
return this.file.type === 'tree';
},
levelIndentation() {
if (this.file.level > 0) {
return {
marginLeft: `${this.file.level * 16}px`,
};
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
},
fileClass() {
if (this.file.type === 'blob') {
if (this.file.active) {
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
} else if (this.file.type === 'tree') {
return 'folder';
}
return '';
},
},
computed: {
isTree() {
return this.file.type === 'tree';
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
isBlob() {
return this.file.type === 'blob';
},
levelIndentation() {
return {
marginLeft: `${this.level * 16}px`,
};
},
methods: {
...mapActions([
'updateDelayViewerUpdated',
]),
clickFile(row) {
// Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
this.$store.dispatch('toggleTreeOpen', {
endpoint: this.file.url,
tree: this.file,
});
}
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active,
folder: this.isTree,
};
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path);
}
const delayPromise = this.file.changed ?
Promise.resolve() : this.updateDelayViewerUpdated(true);
const delayPromise = this.file.changed
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
this.$router.push(`/project${row.url}`);
});
},
return delayPromise.then(() => {
router.push(`/project${this.file.url}`);
});
},
};
},
};
</script>
<template>
......@@ -101,54 +82,45 @@
>
<div
class="file-name"
@click="clickFile(file)"
@click="clickFile"
role="button"
>
<a
<span
class="ide-file-name str-truncated"
:style="levelIndentation"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="file.type === 'tree'"
:folder="isTree"
:opened="file.opened"
:style="levelIndentation"
:size="16"
/>
{{ file.name }}
<file-status-icon :file="file" />
</a>
<file-status-icon
:file="file"
/>
</span>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5 pull-right"
/>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"
/>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5"
class="pull-right prepend-left-8"
/>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</div>
</div>
<template
v-if="file.opened"
>
<template v-if="file.opened">
<repo-file
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
:level="level + 1"
/>
</template>
</div>
......
<script>
import { mapGetters } from 'vuex';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
return this.file.rawPath ||
this.file.blamePath ||
this.file.commitsPath ||
this.file.permalink;
},
rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw';
return this.file.binary ? 'Download' : 'Raw';
},
},
};
......@@ -25,7 +26,7 @@ export default {
class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
:href="file.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
......@@ -38,19 +39,19 @@ export default {
aria-label="File actions"
>
<a
:href="activeFile.blamePath"
:href="file.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="activeFile.commitsPath"
:href="file.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="activeFile.permalink"
:href="file.permalink"
class="btn btn-default btn-sm 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 @@
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
@click.stop.prevent="closeFile(tab.path)"
:aria-label="closeLabel"
>
<icon
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
......@@ -8,29 +8,33 @@
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
},
data() {
return {
showShadow: false,
};
},
computed: {
...mapGetters([
'hasChanges',
]),
...mapState([
'openFiles',
'viewer',
]),
},
updated() {
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: {
...mapActions([
'updateViewer',
]),
...mapActions(['updateViewer']),
},
};
</script>
......@@ -42,7 +46,7 @@
ref="tabsScroller"
>
<repo-tab
v-for="tab in openFiles"
v-for="tab in files"
:key="tab.key"
: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';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
......@@ -76,7 +73,7 @@ router.beforeEach((to, from, next) => {
})
.then(() => {
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) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
......
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -8,11 +10,11 @@ export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
state.changedFiles.forEach(file => {
commit(types.DISCARD_FILE_CHANGES, file.path);
if (file.tempFile) {
dispatch('closeFile', file);
dispatch('closeFile', file.path);
}
});
......@@ -20,20 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file));
};
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);
}
state.openFiles.forEach(file => dispatch('closeFile', file.path));
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
......@@ -49,28 +38,63 @@ export const setResizingStatus = ({ commit }, resizing) => {
};
export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') {
dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
{ state, commit, dispatch },
{ branchId, name, type, content = '', base64 = false },
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
const fullName =
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,
});
if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
}
resolve(file);
});
} else if (type === 'blob') {
dispatch('createTempFile', {
projectId,
worker.postMessage({
data: [fullName],
projectId: state.currentProjectId,
branchId,
parent: selectedParent,
name,
type,
tempFile: true,
base64,
content,
});
}
};
return null;
});
export const scrollToTab = () => {
Vue.nextTick(() => {
......@@ -95,4 +119,3 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
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';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
setPageTitle,
createTemp,
findIndexOfFile,
} from '../utils';
export const closeFile = ({ commit, state, dispatch }, file) => {
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
const file = state.entries[path];
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
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}`);
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
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;
if (file.active) return;
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');
// reset hash for line highlighting
location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
......@@ -54,104 +49,97 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file });
service.getFileData(file.url)
.then((res) => {
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
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)
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ state, commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
export const getRawFileData = ({ commit, dispatch }, file) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
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) {
commit(types.ADD_FILE_TO_CHANGED, file);
commit(types.ADD_FILE_TO_CHANGED, path);
} 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 setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (getters.activeFile) {
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
export const setFileEOL = ({ getters, commit }, { eol }) => {
if (getters.activeFile) {
commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
editorRow,
editorColumn,
});
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
projectId,
branchId,
name: name.replace(`${path}/`, ''),
path,
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 = ({ state, commit }, path) => {
const file = state.entries[path];
export const discardFileChanges = ({ commit }, file) => {
commit(types.DISCARD_FILE_CHANGES, file);
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) {
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.TOGGLE_FILE_OPEN, path);
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
......
......@@ -2,7 +2,6 @@ import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
......@@ -25,3 +24,26 @@ export const getProjectData = (
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 flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
setPageTitle,
findEntry,
createTemp,
createOrMergeEntry,
sortTree,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const getTreeData = (
{ commit, state, dispatch },
{ 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 toggleTreeOpen = ({ commit, dispatch }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, { entry: row });
visitUrl(row.url);
dispatch('toggleTreeOpen', row.path);
} else if (row.type === 'blob' && (row.opened || row.changed)) {
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 {
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) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
......@@ -146,47 +50,6 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.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 = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
......@@ -199,75 +62,25 @@ export const getFiles = (
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const newTree = data.reduce((outputArray, file) => {
const pathSplit = file.split('/');
const blobName = pathSplit.pop();
let selectedFolderTree;
let foundFolder = null;
let fullPath = '';
if (pathSplit.length > 0) {
const newBaseFolders = pathSplit.reduce((newFolders, folder, currentIndex) => {
fullPath += `/${folder}`;
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) {
selectedFolderTree.push(foundFolder);
} else {
newFolders.push(foundFolder);
}
}
selectedFolderTree = foundFolder.tree;
return newFolders;
}, []);
if (newBaseFolders.length) outputArray.push(newBaseFolders[0]);
}
// Add file
const blobEntry = createOrMergeEntry({
projectId,
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,
});
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
if (selectedFolderTree) {
selectedFolderTree.push(blobEntry);
} else {
outputArray.push(blobEntry);
}
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
return outputArray;
}, []);
worker.terminate();
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();
});
resolve();
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch((e) => {
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) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
return state.canCommit &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
export const projectsWithTrees = state =>
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;
......@@ -137,6 +137,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
......
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
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_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
......@@ -20,7 +19,6 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
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 CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
......@@ -35,17 +33,11 @@ export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_EOL = 'SET_FILE_EOL';
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 REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_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_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......@@ -8,25 +8,19 @@ export default {
[types.SET_INITIAL_DATA](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 }) {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
}
},
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
......@@ -57,6 +51,44 @@ export default {
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) {
Object.assign(state, {
viewer,
......
......@@ -7,16 +7,14 @@ export default {
});
},
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties
Object.assign(branch, {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
});
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
[branchName]: {
...branch,
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
},
},
});
},
......
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
});
Object.assign(state, {
selectedFile: file,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
if (file.opened) {
state.openFiles.push(file);
if (state.entries[path].opened) {
state.openFiles.push(state.entries[path]);
} 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 }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
id: data.id,
blamePath: data.blame_path,
commitsPath: data.commits_path,
......@@ -34,53 +31,52 @@ export default {
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
Object.assign(file, {
Object.assign(state.entries[path], {
content,
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: file.raw,
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
[types.ADD_FILE_TO_CHANGED](state, file) {
state.changedFiles.push(file);
[types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
},
[types.REMOVE_FILE_FROM_CHANGED](state, file) {
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
state.changedFiles.splice(indexOfChangedFile, 1);
[types.REMOVE_FILE_FROM_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
changed,
});
},
......
import * as types from '../mutation_types';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
[types.TOGGLE_TREE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
},
[types.CREATE_TREE](state, { treePath }) {
......@@ -16,14 +16,13 @@ export default {
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, {
parentTreeUrl: url,
trees: Object.assign(state.trees, {
[treePath]: {
tree: data,
},
}),
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......@@ -31,9 +30,6 @@ export default {
lastCommitPath: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
[types.REMOVE_ALL_CHANGES_FILES](state) {
Object.assign(state, {
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