Commit 5ee20b63 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'master' into '37970-ci-sections-tracking'

# Conflicts:
#   db/schema.rb
parents c0cfc9eb d4f3963e
......@@ -27,7 +27,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/${CI_PROJECT_NAME}/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script:
- bundle --version
......@@ -87,12 +87,13 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- export ALL_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/all_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/${CI_PROJECT_NAME}/new_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export SUITE_FLAKY_RSPEC_REPORT_PATH=${FLAKY_RSPEC_SUITE_REPORT_PATH}
- export FLAKY_RSPEC_REPORT_PATH=rspec_flaky/all_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export NEW_FLAKY_RSPEC_REPORT_PATH=rspec_flaky/new_${JOB_NAME[0]}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export FLAKY_RSPEC_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- cp ${FLAKY_RSPEC_SUITE_REPORT_PATH} ${ALL_FLAKY_RSPEC_REPORT_PATH}
- '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
......@@ -233,7 +234,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/${CI_PROJECT_NAME}/
- mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
......@@ -252,22 +253,21 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
services: []
before_script: []
cache: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/${CI_PROJECT_NAME}/new_rspec_flaky_examples.json
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
allow_failure: yes
only:
......@@ -281,7 +281,7 @@ flaky-examples-check:
- rspec_flaky/
script:
- '[[ -f $NEW_FLAKY_SPECS_REPORT ]] || echo "{}" > ${NEW_FLAKY_SPECS_REPORT}'
- scripts/merge-reports $NEW_FLAKY_SPECS_REPORT rspec_flaky/${CI_PROJECT_NAME}/new_node_*.json
- scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
......
......@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
- [FIXED] Fix CSRF validation issue when closing/opening merge requests from the UI. !14555
- [FIXED] Kubernetes integration: ensure v1.8.0 compatibility. !14635
- [FIXED] Fixes data parameter not being sent in ajax request for jobs log.
- [FIXED] Improves UX of autodevops popover to match gpg one.
- [FIXED] Fixed commenting on side-by-side commit diff.
- [FIXED] Make sure API responds with 401 when invalid authentication info is provided.
- [FIXED] Fix merge request counter updates after merge.
- [FIXED] Fix gitlab-rake gitlab:import:repos task failing.
- [FIXED] Fix pushes to an empty repository not invalidating has_visible_content? cache.
- [FIXED] Ensure all refs are restored on a restore from backup.
- [FIXED] Gitaly RepositoryExists remains opt-in for all method calls.
- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup.
- [FIXED] Adjust MRs being stuck on "process of being merged" for more than 2 hours.
## 10.0.2 (2017-09-27)
- [FIXED] Notes will not show an empty bubble when the author isn't a member. !14450
......@@ -195,6 +212,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task.
......
......@@ -23,7 +23,7 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
......
......@@ -80,7 +80,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.3.5)
bindata (2.4.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
......@@ -166,9 +166,9 @@ GEM
docile (1.1.5)
domain_name (0.5.20161021)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
doorkeeper (4.2.6)
railties (>= 4.2)
doorkeeper-openid_connect (1.1.2)
doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
......@@ -410,7 +410,7 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
json-jwt (1.7.1)
json-jwt (1.7.2)
activesupport
bindata
multi_json (>= 1.3)
......@@ -681,7 +681,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
rake (12.0.0)
rake (12.1.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
......@@ -910,7 +910,7 @@ GEM
json (>= 1.8.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.1.0)
kgio (~> 2.6)
......@@ -1002,7 +1002,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0)
doorkeeper-openid_connect (~> 1.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
......
<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
import { visitUrl } from './lib/utils/url_utility';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
window.BuildArtifacts = (function() {
function BuildArtifacts() {
this.disablePropagation();
this.setupEntryClick();
this.setupTooltips();
}
BuildArtifacts.prototype.disablePropagation = function() {
......@@ -17,7 +20,26 @@ window.BuildArtifacts = (function() {
BuildArtifacts.prototype.setupEntryClick = function() {
return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
return window.location = this.dataset.link;
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
};
BuildArtifacts.prototype.setupTooltips = function() {
$('.js-artifact-tree-tooltip').tooltip({
placement: 'bottom',
// Stop the tooltip from hiding when we stop hovering the element directly
// We handle all the showing/hiding below
trigger: 'manual',
});
// We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name
$('.js-artifact-tree-row')
.on('mouseenter', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
})
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
});
};
......
/* globals Flash */
import Visibility from 'visibilityjs';
import axios from 'axios';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import './flash';
/**
* Cluster page has 2 separate parts:
* Toggle button
*
* - Polling status while creating or scheduled
* -- Update status area with the response result
*/
class ClusterService {
constructor(options = {}) {
this.options = options;
}
fetchData() {
return axios.get(this.options.endpoint);
}
}
export default class Clusters {
constructor() {
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
this.state = {
statusPath: dataset.statusPath,
clusterStatus: dataset.clusterStatus,
clusterStatusReason: dataset.clusterStatusReason,
toggleStatus: dataset.toggleStatus,
};
this.service = new ClusterService({ endpoint: this.state.statusPath });
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.toggleButton.addEventListener('click', this.toggle.bind(this));
if (this.state.clusterStatus !== 'created') {
this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
}
if (this.state.statusPath) {
this.initPolling();
}
}
toggle() {
this.toggleButton.classList.toggle('checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
}
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: (data) => {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
this.creatingContainer.classList.add('hidden');
}
updateContainer(status, error) {
this.hideAll();
switch (status) {
case 'created':
this.successContainer.classList.remove('hidden');
break;
case 'errored':
this.errorContainer.classList.remove('hidden');
this.errorReasonContainer.textContent = error;
break;
case 'scheduled':
case 'creating':
this.creatingContainer.classList.remove('hidden');
break;
default:
this.hideAll();
}
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
window.Commit = (function() {
function Commit() {
$('.files .diff-file').each(function() {
return new CommitFile(this);
});
}
return Commit;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
new gl.ImageFile(file);
}
}
return CommitFile;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
(function() {
gl.ImageFile = (function() {
var prepareFrames;
......@@ -17,15 +19,10 @@
// Load two-up view after images are loaded
// so that we can display the correct width and height information
const images = $('.two-up.view img', _this.file);
let loadedCount = 0;
images.on('load', () => {
loadedCount += 1;
const $images = $('.two-up.view img', _this.file);
if (loadedCount === images.length) {
$images.waitForImages(function() {
_this.initView('two-up');
}
});
});
};
......
......@@ -20,7 +20,7 @@
<template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && hasDa === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
<template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template>
<template v-else>
--
......
......@@ -3,6 +3,7 @@
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
......@@ -17,9 +18,12 @@ class Diff {
}
});
FilesCommentButton.init($diffFile);
const tab = document.getElementById('diffs');
if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file));
const firstFile = $('.files').first().get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) {
$(document)
......
......@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them.
$target.closest(".content").show();
$target = $target.closest("tr.notes_holder");
const $notesHolder = $target.closest("tr.notes_holder");
// Image diff discussions does not use notes_holder
// so we should keep original $target value in those cases
if ($notesHolder.length > 0) {
$target = $notesHolder;
}
$target.show();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
......
......@@ -7,7 +7,6 @@
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global Commit */
/* global CommitsList */
/* global NewBranchForm */
/* global NotificationsForm */
......@@ -316,7 +315,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new gl.Activities();
break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
......@@ -525,6 +523,11 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors();
break;
case 'projects:clusters:show':
import(/* webpackChunkName: "clusters" */ './clusters')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch(() => {});
break;
}
switch (path[0]) {
case 'sessions':
......
......@@ -738,7 +738,7 @@ GitLabDropdown = (function() {
: selectedObject.id;
if (isInput) {
field = $(this.el);
} else if (value) {
} else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
......@@ -746,7 +746,7 @@ GitLabDropdown = (function() {
return;
}
if (el.hasClass(ACTIVE_CLASS)) {
if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
......@@ -852,7 +852,7 @@ GitLabDropdown = (function() {
if (href && href !== '#') {
gl.utils.visitUrl(href);
} else {
$el.first().trigger('click');
$el.trigger('click');
}
}
};
......
export function createImageBadge(noteId, { x, y }, classNames = []) {
const buttonEl = document.createElement('button');
const classList = classNames.concat(['js-image-badge']);
classList.forEach(className => buttonEl.classList.add(className));
buttonEl.setAttribute('type', 'button');
buttonEl.setAttribute('disabled', true);
buttonEl.dataset.noteId = noteId;
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
return buttonEl;
}
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge']);
buttonEl.innerText = badgeText;
containerEl.appendChild(buttonEl);
}
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
const iconEl = document.createElement('i');
iconEl.className = 'fa fa-comment-o';
iconEl.setAttribute('aria-label', 'comment');
buttonEl.appendChild(iconEl);
containerEl.appendChild(buttonEl);
}
export function addAvatarBadge(el, event) {
const { noteId, badgeNumber } = event.detail;
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
avatarBadgeEl.innerText = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}
export function addCommentIndicator(containerEl, { x, y }) {
const buttonEl = document.createElement('button');
buttonEl.classList.add('btn-transparent');
buttonEl.classList.add('comment-indicator');
buttonEl.setAttribute('type', 'button');
buttonEl.style.left = `${x}px`;
buttonEl.style.top = `${y}px`;
buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
containerEl.appendChild(buttonEl);
}
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
const willRemove = !!commentIndicatorEl;
let meta = {};
if (willRemove) {
meta = {
x: parseInt(commentIndicatorEl.style.left, 10),
y: parseInt(commentIndicatorEl.style.top, 10),
image: {
width: imageEl.width,
height: imageEl.height,
},
};
commentIndicatorEl.remove();
}
return Object.assign({}, meta, {
removed: willRemove,
});
}
export function showCommentIndicator(imageFrameEl, coordinate) {
const { x, y } = coordinate;
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
if (commentIndicatorEl) {
commentIndicatorEl.style.left = `${x}px`;
commentIndicatorEl.style.top = `${y}px`;
} else {
addCommentIndicator(imageFrameEl, coordinate);
}
}
export function commentIndicatorOnClick(event) {
// Prevent from triggering onAddImageDiffNote in notes.js
event.stopPropagation();
const buttonEl = event.currentTarget;
const diffViewerEl = buttonEl.closest('.diff-viewer');
const textareaEl = diffViewerEl.querySelector('.note-container .note-textarea');
textareaEl.focus();
}
export function setPositionDataAttribute(el, options) {
// Update position data attribute so that the
// new comment form can use this data for ajax request
const { x, y, width, height } = options;
const position = el.dataset.position;
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
width,
height,
});
el.setAttribute('data-position', JSON.stringify(positionObject));
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
avatarBadgeEl.innerText = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
discussionBadgeEl.innerText = newBadgeNumber;
}
export function toggleCollapsed(event) {
const toggleButtonEl = event.currentTarget;
const discussionNotesEl = toggleButtonEl.closest('.discussion-notes');
const formEl = discussionNotesEl.querySelector('.discussion-form');
const isCollapsed = discussionNotesEl.classList.contains('collapsed');
if (isCollapsed) {
discussionNotesEl.classList.remove('collapsed');
} else {
discussionNotesEl.classList.add('collapsed');
}
// Override the inline display style set in notes.js
if (formEl && !isCollapsed) {
formEl.style.display = 'none';
} else if (formEl && isCollapsed) {
formEl.style.display = 'block';
}
}
import * as badgeHelper from './badge_helper';
import * as commentIndicatorHelper from './comment_indicator_helper';
import * as domHelper from './dom_helper';
import * as utilsHelper from './utils_helper';
export default {
addCommentIndicator: commentIndicatorHelper.addCommentIndicator,
removeCommentIndicator: commentIndicatorHelper.removeCommentIndicator,
showCommentIndicator: commentIndicatorHelper.showCommentIndicator,
commentIndicatorOnClick: commentIndicatorHelper.commentIndicatorOnClick,
addImageBadge: badgeHelper.addImageBadge,
addImageCommentBadge: badgeHelper.addImageCommentBadge,
addAvatarBadge: badgeHelper.addAvatarBadge,
setPositionDataAttribute: domHelper.setPositionDataAttribute,
updateDiscussionAvatarBadgeNumber: domHelper.updateDiscussionAvatarBadgeNumber,
updateDiscussionBadgeNumber: domHelper.updateDiscussionBadgeNumber,
toggleCollapsed: domHelper.toggleCollapsed,
resizeCoordinatesToImageElement: utilsHelper.resizeCoordinatesToImageElement,
generateBadgeFromDiscussionDOM: utilsHelper.generateBadgeFromDiscussionDOM,
getTargetSelection: utilsHelper.getTargetSelection,
initImageDiff: utilsHelper.initImageDiff,
};
import ImageBadge from '../image_badge';
import ImageDiff from '../image_diff';
import ReplacedImageDiff from '../replaced_image_diff';
import '../../commit/image_file';
export function resizeCoordinatesToImageElement(imageEl, meta) {
const { x, y, width, height } = meta;
const imageWidth = imageEl.width;
const imageHeight = imageEl.height;
const widthRatio = imageWidth / width;
const heightRatio = imageHeight / height;
return {
x: Math.round(x * widthRatio),
y: Math.round(y * heightRatio),
width: imageWidth,
height: imageHeight,
};
}
export function generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl) {
const position = JSON.parse(discussionEl.dataset.position);
const firstNoteEl = discussionEl.querySelector('.note');
const badge = new ImageBadge({
actual: position,
imageEl: imageFrameEl.querySelector('img'),
noteId: firstNoteEl.id,
discussionId: discussionEl.dataset.discussionId,
});
return badge;
}
export function getTargetSelection(event) {
const containerEl = event.currentTarget;
const imageEl = containerEl.querySelector('img');
const x = event.offsetX;
const y = event.offsetY;
const width = imageEl.width;
const height = imageEl.height;
const actualWidth = imageEl.naturalWidth;
const actualHeight = imageEl.naturalHeight;
const widthRatio = actualWidth / width;
const heightRatio = actualHeight / height;
// Browser will include the frame as a clickable target,
// which would result in potential 1px out of bounds value
// This bound the coordinates to inside the frame
const normalizedX = Math.max(0, x) && Math.min(x, width);
const normalizedY = Math.max(0, y) && Math.min(y, height);
return {
browser: {
x: normalizedX,
y: normalizedY,
width,
height,
},
actual: {
// Round x, y so that we don't need to deal with decimals
x: Math.round(normalizedX * widthRatio),
y: Math.round(normalizedY * heightRatio),
width: actualWidth,
height: actualHeight,
},
};
}
export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
const options = {
canCreateNote,
renderCommentBadge,
};
let diff;
// ImageFile needs to be invoked before initImageDiff so that badges
// can mount to the correct location
new gl.ImageFile(fileEl); // eslint-disable-line no-new
if (fileEl.querySelector('.diff-file .js-single-image')) {
diff = new ImageDiff(fileEl, options);
diff.init();
} else if (fileEl.querySelector('.diff-file .js-replaced-image')) {
diff = new ReplacedImageDiff(fileEl, options);
diff.init();
}
return diff;
}
import imageDiffHelper from './helpers/index';
const defaultMeta = {
x: 0,
y: 0,
width: 0,
height: 0,
};
export default class ImageBadge {
constructor(options) {
const { noteId, discussionId } = options;
this.actual = options.actual || defaultMeta;
this.browser = options.browser || defaultMeta;
this.noteId = noteId;
this.discussionId = discussionId;
if (options.imageEl && !options.browser) {
this.browser = imageDiffHelper.resizeCoordinatesToImageElement(options.imageEl, this.actual);
}
}
}
import imageDiffHelper from './helpers/index';
import ImageBadge from './image_badge';
import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
this.canCreateNote = !!(options && options.canCreateNote);
this.renderCommentBadge = !!(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
init() {
this.imageFrameEl = this.el.querySelector('.diff-file .js-image-frame');
this.imageEl = this.imageFrameEl.querySelector('img');
this.bindEvents();
}
bindEvents() {
this.imageClickedWrapper = this.imageClicked.bind(this);
this.imageBlurredWrapper = imageDiffHelper.removeCommentIndicator.bind(null, this.imageFrameEl);
this.addBadgeWrapper = this.addBadge.bind(this);
this.removeBadgeWrapper = this.removeBadge.bind(this);
this.renderBadgesWrapper = this.renderBadges.bind(this);
// Render badges
if (isImageLoaded(this.imageEl)) {
this.renderBadges();
} else {
this.imageEl.addEventListener('load', this.renderBadgesWrapper);
}
// jquery makes the event delegation here much simpler
this.$noteContainer.on('click', '.js-diff-notes-toggle', imageDiffHelper.toggleCollapsed);
$(this.el).on('click', '.comment-indicator', imageDiffHelper.commentIndicatorOnClick);
if (this.canCreateNote) {
this.el.addEventListener('click.imageDiff', this.imageClickedWrapper);
this.el.addEventListener('blur.imageDiff', this.imageBlurredWrapper);
this.el.addEventListener('addBadge.imageDiff', this.addBadgeWrapper);
this.el.addEventListener('removeBadge.imageDiff', this.removeBadgeWrapper);
}
}
imageClicked(event) {
const customEvent = event.detail;
const selection = imageDiffHelper.getTargetSelection(customEvent);
const el = customEvent.currentTarget;
imageDiffHelper.setPositionDataAttribute(el, selection.actual);
imageDiffHelper.showCommentIndicator(this.imageFrameEl, selection.browser);
}
renderBadges() {
const discussionsEls = this.el.querySelectorAll('.note-container .discussion-notes .notes');
[...discussionsEls].forEach(this.renderBadge.bind(this));
}
renderBadge(discussionEl, index) {
const imageBadge = imageDiffHelper
.generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
this.imageBadges.push(imageBadge);
const options = {
coordinate: imageBadge.browser,
noteId: imageBadge.noteId,
};
if (this.renderCommentBadge) {
imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
} else {
const numberBadgeOptions = Object.assign({}, options, {
badgeText: index + 1,
});
imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
}
}
addBadge(event) {
const { x, y, width, height, noteId, discussionId } = event.detail;
const badgeText = this.imageBadges.length + 1;
const imageBadge = new ImageBadge({
actual: {
x,
y,
width,
height,
},
imageEl: this.imageFrameEl.querySelector('img'),
noteId,
discussionId,
});
this.imageBadges.push(imageBadge);
imageDiffHelper.addImageBadge(this.imageFrameEl, {
coordinate: imageBadge.browser,
badgeText,
noteId,
});
imageDiffHelper.addAvatarBadge(this.el, {
detail: {
noteId,
badgeNumber: badgeText,
},
});
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, badgeText);
}
removeBadge(event) {
const { badgeNumber } = event.detail;
const indexToRemove = badgeNumber - 1;
const imageBadgeEls = this.imageFrameEl.querySelectorAll('.badge');
if (this.imageBadges.length !== badgeNumber) {
// Cascade badges count numbers for (avatar badges + image badges)
this.imageBadges.forEach((badge, index) => {
if (index > indexToRemove) {
const { discussionId } = badge;
const updatedBadgeNumber = index;
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
imageBadgeEls[index].innerText = updatedBadgeNumber;
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
}
});
}
this.imageBadges.splice(indexToRemove, 1);
const imageBadgeEl = imageBadgeEls[indexToRemove];
imageBadgeEl.remove();
}
}
import imageDiffHelper from './helpers/index';
export default () => {
// Always pass can-create-note as false because a user
// cannot place new badge markers on discussion tab
const canCreateNote = false;
const renderCommentBadge = true;
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl =>
imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
};
import imageDiffHelper from './helpers/index';
import { viewTypes, isValidViewType } from './view_types';
import ImageDiff from './image_diff';
export default class ReplacedImageDiff extends ImageDiff {
init(defaultViewType = viewTypes.TWO_UP) {
this.imageFrameEls = {
[viewTypes.TWO_UP]: this.el.querySelector('.two-up .js-image-frame'),
[viewTypes.SWIPE]: this.el.querySelector('.swipe .js-image-frame'),
[viewTypes.ONION_SKIN]: this.el.querySelector('.onion-skin .js-image-frame'),
};
const viewModesEl = this.el.querySelector('.view-modes-menu');
this.viewModesEls = {
[viewTypes.TWO_UP]: viewModesEl.querySelector('.two-up'),
[viewTypes.SWIPE]: viewModesEl.querySelector('.swipe'),
[viewTypes.ONION_SKIN]: viewModesEl.querySelector('.onion-skin'),
};
this.currentView = defaultViewType;
this.generateImageEls();
this.bindEvents();
}
generateImageEls() {
this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
viewTypeNames.forEach((viewType) => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
});
}
bindEvents() {
super.bindEvents();
this.changeToViewTwoUp = this.changeView.bind(this, viewTypes.TWO_UP);
this.changeToViewSwipe = this.changeView.bind(this, viewTypes.SWIPE);
this.changeToViewOnionSkin = this.changeView.bind(this, viewTypes.ONION_SKIN);
this.viewModesEls[viewTypes.TWO_UP].addEventListener('click', this.changeToViewTwoUp);
this.viewModesEls[viewTypes.SWIPE].addEventListener('click', this.changeToViewSwipe);
this.viewModesEls[viewTypes.ONION_SKIN].addEventListener('click', this.changeToViewOnionSkin);
}
get imageEl() {
return this.imageEls[this.currentView];
}
get imageFrameEl() {
return this.imageFrameEls[this.currentView];
}
changeView(newView) {
if (!isValidViewType(newView)) {
return;
}
const indicator = imageDiffHelper.removeCommentIndicator(this.imageFrameEl);
this.currentView = newView;
// Clear existing badges on new view
const existingBadges = this.imageFrameEl.querySelectorAll('.badge');
[...existingBadges].map(badge => badge.remove());
// Remove existing references to old view image badges
this.imageBadges = [];
// Image_file.js has a fade animation of 200ms for loading the view
// Need to wait an additional 250ms for the images to be displayed
// on window in order to re-normalize their dimensions
setTimeout(this.renderNewView.bind(this, indicator), 250);
}
renderNewView(indicator) {
// Generate badge coordinates on new view
this.renderBadges();
// Re-render indicator in new view
if (indicator.removed) {
const normalizedIndicator = imageDiffHelper
.resizeCoordinatesToImageElement(this.imageEl, {
x: indicator.x,
y: indicator.y,
width: indicator.image.width,
height: indicator.image.height,
});
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
}
}
}
export const viewTypes = {
TWO_UP: 'TWO_UP',
SWIPE: 'SWIPE',
ONION_SKIN: 'ONION_SKIN',
};
export function isValidViewType(validate) {
return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
}
......@@ -14,6 +14,9 @@ If you need to compose a headers object, use the spread operator:
someOtherHeader: '12345',
}
```
see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62
*/
const csrf = {
......@@ -53,4 +56,3 @@ if ($.rails) {
}
export default csrf;
/* eslint-disable import/prefer-default-export */
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
var base;
var w = window;
if (w.gl == null) {
......@@ -86,6 +87,21 @@ w.gl.utils.getLocationHash = function(url) {
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.visitUrl = (url) => {
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
// See https://mathiasbynens.github.io/rel-noopener/
const otherWindow = window.open();
otherWindow.opener = null;
otherWindow.location = url;
} else {
document.location.href = url;
}
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
visitUrl,
};
......@@ -54,12 +54,14 @@ LineHighlighter.prototype.bindEvents = function() {
$fileHolder.on('highlight:line', this.highlightHash);
};
LineHighlighter.prototype.highlightHash = function() {
var range;
LineHighlighter.prototype.highlightHash = function(newHash) {
let range;
if (newHash && typeof newHash === 'string') this._hash = newHash;
this.clearHighlight();
if (this._hash !== '') {
range = this.hashToRange(this._hash);
if (range[0]) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
......
......@@ -4,6 +4,7 @@ import sprintf from './sprintf';
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(window.translations || {});
delete window.translations;
/**
Translates `text`
......
......@@ -35,12 +35,9 @@ import './shortcuts_network';
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
// commit
import './commit/file';
import './commit/image_file';
// lib/utils
import './lib/utils/bootstrap_linked_tabs';
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
......@@ -71,7 +68,6 @@ import './build';
import './build_artifacts';
import './build_variables';
import './ci_lint_editor';
import './commit';
import './commits';
import './compare';
import './compare_autocomplete';
......@@ -111,7 +107,6 @@ import './merge_request';
import './merge_request_tabs';
import './milestone';
import './milestone_select';
import './mini_pipeline_graph_dropdown';
import './namespace_select';
import './new_branch_form';
import './new_commit_form';
......@@ -119,7 +114,6 @@ import './notes';
import './notifications_dropdown';
import './notifications_form';
import './pager';
import './pipelines';
import './preview_markdown';
import './project';
import './project_avatar';
......
......@@ -13,6 +13,8 @@ import {
isMetaClick,
} from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
/* eslint-disable max-len */
// MergeRequestTabs
//
......@@ -154,6 +156,8 @@ import {
}
this.resetViewContainer();
this.destroyPipelinesView();
initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
......
......@@ -29,6 +29,7 @@
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {},
};
},
......@@ -64,6 +65,10 @@
this.updatedAspectRatios = 0;
}
},
hoverChanged(data) {
this.hoverData = data;
},
},
created() {
......@@ -72,10 +77,12 @@
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
},
......@@ -102,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics"
:key="index"
:graph-data="graphData"
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
......
......@@ -73,34 +73,22 @@
<template>
<div class="prometheus-state">
<div class="row">
<div class="col-md-4 col-md-offset-4 state-svg svg-content">
<div class="state-svg svg-content">
<img :src="currentState.svgUrl"/>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h4 class="text-center state-title">
<h4 class="state-title">
{{currentState.title}}
</h4>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="description-text text-center state-description">
<p class="state-description">
{{currentState.description}}
<a v-if="showButtonDescription" :href="settingsPath">
Prometheus server
</a>
</div>
</div>
</div>
<div class="row state-button-section">
<div class="col-md-4 col-md-offset-4 text-center state-button">
</p>
<div class="state-button">
<a class="btn btn-success" :href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div>
</div>
</template>
......@@ -3,16 +3,14 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
import GraphPath from './graph_path.vue';
import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { timeScaleFormat } from '../utils/date_time_formatters';
import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
export default {
props: {
graphData: {
......@@ -27,6 +25,11 @@
type: Array,
required: true,
},
hoverData: {
type: Object,
required: false,
default: () => ({}),
},
},
mixins: [MonitoringMixin],
......@@ -52,6 +55,7 @@
currentXCoordinate: 0,
currentFlagPosition: 0,
showFlag: false,
showFlagContent: false,
showDeployInfo: true,
timeSeries: [],
};
......@@ -65,7 +69,7 @@
},
computed: {
outterViewBox() {
outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
......@@ -122,36 +126,30 @@
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0;
this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
eventHub.$emit('hoverChanged', {
hoveredDate,
currentDeployXPos,
});
},
renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0],
this.timeSeries = createTimeSeries(
this.graphData.queries[0],
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
this.graphHeightOffset,
);
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale()
.range([0, this.graphWidth]);
.range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
......@@ -194,6 +192,10 @@
eventHub.$emit('toggleAspectRatio');
}
},
hoverData() {
this.positionFlag();
},
},
mounted() {
......@@ -203,7 +205,10 @@
</script>
<template>
<div class="prometheus-graph">
<div
class="prometheus-graph"
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false">
<h5 class="text-center graph-title">
{{graphData.title}}
</h5>
......@@ -211,7 +216,7 @@
class="prometheus-svg-container"
:style="paddingBottomRootSvg">
<svg
:viewBox="outterViewBox"
:viewBox="outerViewBox"
ref="baseSvg">
<g
class="x-axis"
......@@ -247,6 +252,7 @@
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
......@@ -257,6 +263,7 @@
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/>
<rect
class="prometheus-graph-overlay"
......
......@@ -19,6 +19,10 @@
type: Number,
required: true,
},
graphWidth: {
type: Number,
required: true,
},
},
computed: {
......@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) {
xPosition = -97;
}
return xPosition;
},
},
};
</script>
......@@ -77,7 +89,7 @@
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
x="3"
:x="positionFlag(deployment)"
y="0"
width="92"
height="60">
......
......@@ -23,6 +23,10 @@
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
},
data() {
......@@ -57,6 +61,7 @@
transform="translate(-5, 20)">
</line>
<svg
v-if="showFlagContent"
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
......
......@@ -79,7 +79,11 @@
},
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
const value = series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
......
import { bisectDate } from '../utils/date_time_formatters';
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
......@@ -18,6 +20,7 @@ const mixins = {
return dataFound;
},
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
......@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray;
}, []);
},
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
},
},
};
......
......@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
Dashboard,
},
render: createElement => createElement('dashboard'),
render: createElement => createElement(Dashboard),
}));
......@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
value,
value: Number(value),
})),
})),
})),
......
......@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()],
......
......@@ -56,12 +56,16 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const defined = d => !isNaN(d.value) && d.value != null;
const lineFunction = d3.svg.line()
.defined(defined)
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
.defined(defined)
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
......
......@@ -24,6 +24,7 @@ import './autosave';
import './dropzone_input';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize;
window.Dropzone = Dropzone;
......@@ -42,6 +43,7 @@ export default class Notes {
this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.onAddDiffNote = this.onAddDiffNote.bind(this);
this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this);
......@@ -114,6 +116,8 @@ export default class Notes {
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
$(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
......@@ -140,6 +144,7 @@ export default class Notes {
$(document).off('click', '.js-note-attachment-delete');
$(document).off('click', '.js-discussion-reply-button');
$(document).off('click', '.js-add-diff-note-button');
$(document).off('click', '.js-add-image-diff-note-button');
$(document).off('visibilitychange');
$(document).off('keyup input', '.js-note-text');
$(document).off('click', '.js-note-target-reopen');
......@@ -412,6 +417,11 @@ export default class Notes {
this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
row = form.closest('tr');
if (noteEntity.on_image) {
row = form;
}
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
......@@ -423,7 +433,7 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
......@@ -449,6 +459,7 @@ export default class Notes {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
......@@ -561,7 +572,7 @@ export default class Notes {
form.find('#note_line_code').val(),
// DiffNote
form.find('#note_position').val()
form.find('#note_position').val(),
];
return new Autosave(textarea, key);
}
......@@ -783,9 +794,22 @@ export default class Notes {
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) {
// notesTr does not exist for image diffs
if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) {
const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
detail: {
// badgeNumber's start with 1 and index starts with 0
badgeNumber: $notes.index() + 1,
},
});
$diffFile[0].dispatchEvent(removeBadgeEvent);
}
$notes.remove();
} else {
} else if (notesTr.length > 0) {
notesTr.remove();
}
}
......@@ -841,7 +865,11 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
const diffFileData = dataHolder.closest('.text-file');
let diffFileData = dataHolder.closest('.text-file');
if (diffFileData.length === 0) {
diffFileData = dataHolder.closest('.image');
}
var discussionID = dataHolder.data('discussionId');
......@@ -907,6 +935,31 @@ export default class Notes {
});
}
onAddImageDiffNote(e) {
const $link = $(e.currentTarget || e.target);
const $diffFile = $link.closest('.diff-file');
const clickEvent = new CustomEvent('click.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(clickEvent);
// Setup comment form
let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
newForm = this.cleanForm(this.formClone.clone());
newForm.appendTo($noteContainer);
} else {
newForm = $form;
}
this.setupDiscussionNoteForm($link, newForm);
}
toggleDiffNote({
target,
lineType,
......@@ -999,10 +1052,25 @@ export default class Notes {
}
cancelDiscussionForm(e) {
var form;
e.preventDefault();
form = $(e.target).closest('.js-discussion-note-form');
return this.removeDiscussionNoteForm(form);
const $form = $(e.target).closest('.js-discussion-note-form');
const $discussionNote = $(e.target).closest('.discussion-notes');
if ($discussionNote.length === 0) {
// Only send blur event when the discussion form
// is not part of a discussion note
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
}
return this.removeDiscussionNoteForm($form);
}
/**
......@@ -1414,6 +1482,15 @@ export default class Notes {
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
// Reset cached commands list when command is applied
if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
......@@ -1436,7 +1513,28 @@ export default class Notes {
}
// Show final note element on UI
this.addDiscussionNote($form, note, $notesContainer.length === 0);
const isNewDiffComment = $notesContainer.length === 0;
this.addDiscussionNote($form, note, isNewDiffComment);
if (isNewDiffComment) {
// Add image badge, avatar badge and toggle discussion badge for new image diffs
const notePosition = $form.find('#note_position').val();
if ($diffFile.length > 0 && notePosition.length > 0) {
const { x, y, width, height } = JSON.parse(notePosition);
const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
detail: {
x,
y,
width,
height,
noteId: `note_${note.id}`,
discussionId: note.discussion_id,
},
});
$diffFile[0].dispatchEvent(addBadgeEvent);
}
}
// append flash-container to the Notes list
if ($notesContainer.length) {
......@@ -1457,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
const closestDiffFile = $form.closest('.diff-file');
if (closestDiffFile.length) {
closestDiffFile[0].dispatchEvent(blurEvent);
}
if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove();
}
......@@ -1500,6 +1608,8 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form);
const $diffFile = $form.closest('.diff-file');
const $notesContainer = $form.closest('.notes');
// Cache original comment content
const cachedNoteBodyText = $noteBodyText.html();
......
......@@ -7,10 +7,12 @@
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueCommentForm',
......@@ -26,8 +28,9 @@
};
},
components: {
confidentialIssue,
issueWarning,
issueNoteSignedOutWidget,
issueDiscussionLockedWidget,
markdownField,
userAvatarLink,
},
......@@ -55,6 +58,9 @@
isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
canCreateNote() {
return this.getIssueData.current_user.can_create_note;
},
issueActionButtonTitle() {
if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen';
......@@ -90,9 +96,6 @@
endpoint() {
return this.getIssueData.create_note_path;
},
isConfidentialIssue() {
return this.getIssueData.confidential;
},
},
methods: {
...mapActions([
......@@ -220,6 +223,9 @@
});
},
},
mixins: [
issuableStateMixin,
],
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
......@@ -235,6 +241,7 @@
<template>
<div>
<issue-note-signed-out-widget v-if="!isLoggedIn" />
<issue-discussion-locked-widget v-else-if="!canCreateNote" />
<ul
v-else
class="notes notes-form timeline">
......@@ -253,15 +260,22 @@
<div class="timeline-content timeline-content-form">
<form
ref="commentForm"
class="new-note js-quick-submit common-note-form gfm-form js-main-target-form">
<confidentialIssue v-if="isConfidentialIssue" />
class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
>
<div class="error-alert"></div>
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
:is-confidential-issue="isConfidentialIssue"
ref="markdownField">
<textarea
id="note-body"
......
<script>
export default {
computed: {
lockIcon() {
return gl.utils.spriteIcon('lock');
},
},
};
</script>
<template>
<div class="disabled-comment text-center">
<span class="issuable-note-warning">
<span class="icon" v-html="lockIcon"></span>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueNoteForm',
......@@ -39,12 +40,13 @@
};
},
components: {
confidentialIssue,
issueWarning,
markdownField,
},
computed: {
...mapGetters([
'getDiscussionLastNote',
'getIssueData',
'getIssueDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
......@@ -67,9 +69,6 @@
isDisabled() {
return !this.note.length || this.isSubmitting;
},
isConfidentialIssue() {
return this.getIssueDataByProp('confidential');
},
},
methods: {
handleUpdate() {
......@@ -95,6 +94,9 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
},
},
mixins: [
issuableStateMixin,
],
mounted() {
this.$refs.textarea.focus();
},
......@@ -125,7 +127,13 @@
<div class="flash-container timeline-content"></div>
<form
class="edit-note common-note-form js-quick-submit gfm-form">
<confidentialIssue v-if="isConfidentialIssue" />
<issue-warning
v-if="hasWarning(getIssueData)"
:is-locked="isLocked(getIssueData)"
:is-confidential="isConfidential(getIssueData)"
/>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
......
export default {
methods: {
isConfidential(issue) {
return !!issue.confidential;
},
isLocked(issue) {
return !!issue.discussion_locked;
},
hasWarning(issue) {
return this.isConfidential(issue) || this.isLocked(issue);
},
},
};
......@@ -72,6 +72,13 @@
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
class="js-pipeline-url-failure label label-danger"
:title="pipeline.failure_reason">
error
</span>
<a
v-if="pipeline.flags.auto_devops"
tabindex="0"
......
<script>
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import { __, s__, sprintf } from '../../../locale';
import csrf from '../../../lib/utils/csrf';
export default {
props: {
actionUrl: {
type: String,
required: true,
},
confirmWithPassword: {
type: Boolean,
required: true,
},
username: {
type: String,
required: true,
},
},
data() {
return {
enteredPassword: '',
enteredUsername: '',
isOpen: false,
};
},
components: {
popupDialog,
},
computed: {
csrfToken() {
return csrf.token;
},
inputLabel() {
let confirmationValue;
if (this.confirmWithPassword) {
confirmationValue = __('password');
} else {
confirmationValue = __('username');
}
confirmationValue = `<code>${confirmationValue}</code>`;
return sprintf(
s__('Profiles|Type your %{confirmationValue} to confirm:'),
{ confirmationValue },
false,
);
},
text() {
return sprintf(
s__(`Profiles|
You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
},
false,
);
},
},
methods: {
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
return this.enteredUsername === this.username;
},
onSubmit(status) {
if (status) {
if (!this.canSubmit()) {
return;
}
this.$refs.form.submit();
}
this.toggleOpen(false);
},
toggleOpen(isOpen) {
this.isOpen = isOpen;
},
},
};
</script>
<template>
<div>
<popup-dialog
v-if="isOpen"
:title="s__('Profiles|Delete your account?')"
:text="text"
:kind="`danger ${!canSubmit() && 'disabled'}`"
:primary-button-label="s__('Profiles|Delete account')"
@toggle="toggleOpen"
@submit="onSubmit">
<template slot="body" scope="props">
<p v-html="props.text"></p>
<form
ref="form"
:action="actionUrl"
method="post">
<input
type="hidden"
name="_method"
value="delete" />
<input
type="hidden"
name="authenticity_token"
:value="csrfToken" />
<p id="input-label" v-html="inputLabel"></p>
<input
v-if="confirmWithPassword"
name="password"
class="form-control"
type="password"
v-model="enteredPassword"
aria-labelledby="input-label" />
<input
v-else
name="username"
class="form-control"
type="text"
v-model="enteredUsername"
aria-labelledby="input-label" />
</form>
</template>
</popup-dialog>
<button
type="button"
class="btn btn-danger"
@click="toggleOpen(true)">
{{ s__('Profiles|Delete account') }}
</button>
</div>
</template>
import Vue from 'vue';
import deleteAccountModal from './components/delete_account_modal.vue';
const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new
new Vue({
el: deleteAccountModalEl,
components: {
deleteAccountModal,
},
render(createElement) {
return createElement('delete-account-modal', {
props: {
actionUrl: deleteAccountModalEl.dataset.actionUrl,
confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
username: deleteAccountModalEl.dataset.username,
},
});
},
});
export default () => {
$('.fork-thumbnail a').on('click', function forkThumbnailClicked() {
$('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
$('.fork-namespaces').hide();
return $('.save-project-loader').show();
return $('.js-fork-content').toggle();
});
};
import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
export default class ProtectedBranchCreate {
constructor() {
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
......@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
});
// Select default
$allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
$allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected branch dropdown
this.protectedBranchDropdown = new ProtectedBranchDropdown({
$dropdown: this.$form.find('.js-protected-branch-select'),
$dropdown: $protectedBranchDropdown,
onSelect: this.onSelectCallback,
});
this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
}
// This will run after clicked callback
......@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate {
const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
$allowedToPushInput.length
);
this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
this.$form.find('input[type="submit"]').attr('disabled', completedForm);
}
loadPreviousSelection(mergeDropdown, pushDropdown) {
let mergeIndex = 0;
let pushIndex = 0;
if (this.isLocalStorageAvailable) {
const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
if (savedDefaults != null) {
mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
id: parseInt(savedDefaults.mergeSelection, 0),
});
pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
id: parseInt(savedDefaults.pushSelection, 0),
});
}
}
mergeDropdown.selectRowAtIndex(mergeIndex);
pushDropdown.selectRowAtIndex(pushIndex);
}
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
savePreviousSelection(mergeSelection, pushSelection) {
if (this.isLocalStorageAvailable) {
const branchDefaults = {
mergeSelection,
pushSelection,
};
window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
}
}
}
......@@ -62,7 +62,7 @@ export default {
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
......
......@@ -63,12 +63,7 @@ const RepoEditor = {
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber;
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
Store.setActiveLine(lineNumber);
}
},
},
......@@ -101,6 +96,15 @@ const RepoEditor = {
this.setupEditor();
}
},
activeLine() {
if (Helper.monacoInstance) {
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
}
},
},
computed: {
shouldHideEditor() {
......
......@@ -14,6 +14,11 @@ export default {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
},
mounted() {
this.highlightFile();
......@@ -26,8 +31,12 @@ export default {
html() {
this.$nextTick(() => {
this.highlightFile();
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
},
};
</script>
......
......@@ -18,22 +18,40 @@ export default {
},
created() {
this.addPopEventListener();
window.addEventListener('popstate', this.checkHistory);
},
destroyed() {
window.removeEventListener('popstate', this.checkHistory);
},
data: () => Store,
methods: {
addPopEventListener() {
window.addEventListener('popstate', () => {
if (location.href.indexOf('#') > -1) return;
this.linkClicked({
checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
if (!selectedFile) {
// Maybe it is not in the current tree but in the opened tabs
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href,
});
});
}, lineNumber);
}
},
fileClicked(clickedFile) {
fileClicked(clickedFile, lineNumber) {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
......@@ -41,17 +59,20 @@ export default {
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
Store.setActiveLine(lineNumber);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
......@@ -74,8 +95,8 @@ export default {
<thead v-if="!isMini">
<tr>
<th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th>
<th class="hidden-xs last-update text-right">Last Update</th>
<th class="hidden-sm hidden-xs last-commit">Last commit</th>
<th class="hidden-xs last-update text-right">Last update</th>
</tr>
</thead>
<tbody>
......
......@@ -254,7 +254,9 @@ const RepoHelper = {
RepoHelper.key = RepoHelper.genKey();
if (document.location.pathname !== url) {
history.pushState({ key: RepoHelper.key }, '', url);
}
if (title) {
document.title = title;
......
......@@ -26,7 +26,7 @@ const RepoStore = {
},
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: 0,
activeLine: -1,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
......@@ -85,6 +85,7 @@ const RepoStore = {
if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1);
},
setFileActivity(file, openedFile, i) {
......@@ -101,6 +102,10 @@ const RepoStore = {
RepoStore.activeFileIndex = i;
},
setActiveLine(activeLine) {
if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
},
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
......
......@@ -47,9 +47,9 @@ export default {
</script>
<template>
<div class="block confidentiality">
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
<i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
<i class="fa" :class="faEye" aria-hidden="true"></i>
</div>
<div class="title hide-collapsed">
Confidentiality
......@@ -62,19 +62,19 @@ export default {
Edit
</a>
</div>
<div class="value confidential-value hide-collapsed">
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
<i class="fa fa-eye sidebar-item-icon"></i>
Not confidential
</div>
<div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
<div v-else class="value sidebar-item-value hide-collapsed">
<i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
This issue is confidential
</div>
</div>
......
......@@ -2,9 +2,6 @@
import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
editFormButtons,
},
props: {
isConfidential: {
required: true,
......@@ -19,12 +16,16 @@ export default {
type: Function,
},
},
components: {
editFormButtons,
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu confidential-warning-message">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential">
You are going to turn on the confidentiality. This means that only team members with
......
......@@ -15,7 +15,7 @@ export default {
},
},
computed: {
onOrOff() {
toggleButtonText() {
return this.isConfidential ? 'Turn Off' : 'Turn On';
},
updateConfidentialBool() {
......@@ -26,7 +26,7 @@ export default {
</script>
<template>
<div class="confidential-warning-message-actions">
<div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
......@@ -39,7 +39,7 @@ export default {
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
>
{{ onOrOff }}
{{ toggleButtonText }}
</button>
</div>
</template>
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editFormButtons,
},
};
</script>
<template>
<div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message">
<p class="text" v-if="isLocked">
Unlock this {{ issuableDisplayName(issuableType) }}?
<strong>Everyone</strong>
will be able to comment.
</p>
<p class="text" v-else>
Lock this {{ issuableDisplayName(issuableType) }}?
Only
<strong>project members</strong>
will be able to comment.
</p>
<edit-form-buttons
:is-locked="isLocked"
:toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
</div>
</template>
<script>
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
},
computed: {
buttonText() {
return this.isLocked ? this.__('Unlock') : this.__('Lock');
},
toggleLock() {
return !this.isLocked;
},
},
};
</script>
<template>
<div class="sidebar-item-warning-message-actions">
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
@click.prevent="updateLockedAttribute(toggleLock)"
>
{{ buttonText }}
</button>
</div>
</template>
<script>
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
export default {
props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
issuableType: {
required: true,
type: String,
},
},
mixins: [
issuableMixin,
],
components: {
editForm,
},
computed: {
lockIconClass() {
return this.isLocked ? 'fa-lock' : 'fa-unlock';
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
},
methods: {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
updateLockedAttribute(locked) {
this.mediator.service.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}`)));
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
<i
class="fa"
:class="lockIconClass"
aria-hidden="true"
></i>
</div>
<div class="title hide-collapsed">
Lock {{issuableDisplayName(issuableType) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
type="button"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</button>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
:toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
/>
<div
v-if="isLocked"
class="value sidebar-item-value"
>
<i
aria-hidden="true"
class="fa fa-lock sidebar-item-icon is-active"
></i>
{{ __('Locked') }}
</div>
<div
v-else
class="no-value sidebar-item-value hide-collapsed"
>
<i
aria-hidden="true"
class="fa fa-unlock sidebar-item-icon"
></i>
{{ __('Unlocked') }}
</div>
</div>
</div>
</template>
import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees';
import confidential from './components/confidential/confidential_issue_sidebar.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
Vue.use(Translate);
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
const confidentialEl = document.querySelector('#js-confidential-entry-point');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
}
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
if (!el) return;
if (confidentialEl) {
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(confidential);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
propsData: {
......@@ -31,16 +26,51 @@ function domContentLoaded() {
isEditable: initialData.is_editable,
service: mediator.service,
},
}).$mount(confidentialEl);
}).$mount(el);
}
function mountLockComponent(mediator) {
const el = document.getElementById('js-lock-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const LockComp = Vue.extend(LockIssueSidebar);
new LockComp({
propsData: {
isLocked: initialData.is_locked,
isEditable: initialData.is_editable,
mediator,
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}).$mount(el);
}
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
mediator.fetch();
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
......
......@@ -15,6 +15,7 @@ export default class SidebarStore {
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
SidebarStore.singleton = this;
}
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
......@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl.diffNotesCompileComponents();
}
FilesCommentButton.init($(_this.file));
const $file = $(_this.file);
FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb();
};
......
......@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
return failedClass;
return 'failed';
} else if (!pipeline) {
return defaultClass;
return 'success';
} else if (isPipelineActive) {
return inActionClass;
return 'pending';
} else if (isPipelineFailed) {
return 'failed';
}
return 'success';
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
if (this.status === 'failed') {
return failedClass;
} else if (this.status === 'pending') {
return inActionClass;
}
return defaultClass;
},
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
return 'failed';
}
return 'success';
},
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
......@@ -84,13 +100,8 @@ export default {
},
},
methods: {
isMergeAllowed() {
return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
this.mr.isPipelinePassing ||
this.mr.isPipelineSkipped;
},
shouldShowMergeControls() {
return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText;
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
......@@ -209,7 +220,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" />
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
......
......@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related
......
<script>
export default {
name: 'confidentialIssueWarning',
};
</script>
<template>
<div class="confidential-issue-warning">
<i
aria-hidden="true"
class="fa fa-eye-slash">
</i>
<span>
This is a confidential issue. Your comment will not be visible to the public.
</span>
</div>
</template>
<script>
export default {
props: {
isLocked: {
type: Boolean,
default: false,
required: false,
},
isConfidential: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
iconClass() {
return {
'fa-eye-slash': this.isConfidential,
'fa-lock': this.isLocked,
};
},
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
},
};
</script>
<template>
<div class="issuable-note-warning">
<i
aria-hidden="true"
class="fa icon"
:class="iconClass"
v-if="!isLockedAndConfidential"
></i>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
{{ __('People without permission will never get a notification and won\'t be able to comment.') }}
</span>
<span v-else-if="isConfidential">
{{ __('This is a confidential issue.') }}
{{ __('Your comment will not be visible to the public.') }}
</span>
<span v-else-if="isLocked">
{{ __('This issue is locked.') }}
{{ __('Only project members can comment.') }}
</span>
</div>
</template>
......@@ -7,7 +7,7 @@ export default {
type: String,
required: true,
},
body: {
text: {
type: String,
required: true,
},
......@@ -63,7 +63,9 @@ export default {
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<p>{{this.body}}</p>
<slot name="body" :text="text">
<p>{{text}}</p>
</slot>
</div>
<div class="modal-footer">
<button
......
export default {
methods: {
issuableDisplayName(issuableType) {
const displayName = issuableType.replace(/_/, ' ');
return this.__ ? this.__(displayName) : displayName;
},
},
};
......@@ -40,6 +40,7 @@
@import "framework/tables";
@import "framework/notes";
@import "framework/timeline";
@import "framework/tooltips";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
......
......@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); }
......@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
......
@mixin btn-comment-icon {
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover,
&.inverted {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
}
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
......@@ -381,7 +403,11 @@
background: transparent;
border: 0;
&:hover,
&:active,
&:focus {
outline: 0;
background: transparent;
box-shadow: none;
}
}
......@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
......@@ -129,11 +130,6 @@ span.update-author {
}
}
.user-mention {
color: $user-mention-color;
font-weight: $gl-font-weight-bold;
}
.field_with_errors {
display: inline;
}
......
......@@ -6,3 +6,14 @@
.gfm-commit_range {
@extend .commit-sha;
}
.gfm-project_member {
padding: 0 2px;
border-radius: #{$border-radius-default / 2};
background-color: $user-mention-bg;
&:hover {
background-color: $user-mention-bg-hover;
text-decoration: none;
}
}
......@@ -95,7 +95,7 @@
}
}
.title {
.navbar .title {
> a {
&:hover,
&:focus {
......
.modal-header {
padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title {
margin-top: 0;
}
}
.modal-body {
position: relative;
padding: 15px;
padding: #{3 * $grid-size} #{2 * $grid-size};
.form-actions {
margin: -$gl-padding + 1;
margin-top: 15px;
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
}
.text-danger {
......
......@@ -48,31 +48,24 @@
}
&:hover {
background-color: $white-normal;
border-color: $border-white-normal;
border-color: $gray-darkest;
color: $gl-text-color;
}
}
}
.select2-drop {
box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
.select2-drop,
.select2-drop.select2-drop-above {
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px;
color: $gl-text-color;
}
.select2-results .select2-result-label,
.select2-more-results {
padding: 10px 15px;
}
.select2-drop {
color: $gl-grayish-blue;
}
.select2-highlighted {
background: $gl-link-color !important;
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
}
.select2-results li.select2-result-with-children > .select2-result-label {
......@@ -87,13 +80,11 @@
}
}
.select2-dropdown-open {
.select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice {
border-color: $border-white-normal;
border-color: $gray-darkest;
outline: 0;
background-image: none;
background-color: $white-dark;
box-shadow: $gl-btn-active-gradient;
}
}
......@@ -131,28 +122,14 @@
}
}
}
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
box-shadow: $gl-btn-active-gradient;
}
}
.select2-drop-active {
margin-top: 6px;
margin-top: $dropdown-vertical-offset;
font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-results {
max-height: 350px;
.select2-highlighted {
background: $gl-primary;
}
}
}
......@@ -186,19 +163,35 @@
background-size: 16px 16px !important;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results {
margin: 0;
padding: 10px 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
}
.ajax-users-select {
......@@ -265,56 +258,10 @@
min-width: 250px !important;
}
// TODO: change global style
.ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:edit"] #select2-drop,
body[data-page="projects:new"] #select2-drop,
body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop,
body[data-page="admin:groups:show"] #select2-drop,
body[data-page="projects:issues:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color;
}
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results {
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
}
.select2-result {
padding: 0 1px;
.select2-result-selectable,
.select2-result-unselectable {
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
.select2-result-label {
padding: #{$gl-padding / 2} $gl-padding;
}
&.select2-highlighted {
background-color: transparent !important;
color: $gl-text-color;
.select2-result-label {
background-color: $dropdown-item-hover-bg;
}
}
}
}
}
......@@ -17,15 +17,19 @@
.diff-file {
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
}
&.text-file .diff-file {
border-bottom: none;
}
}
.timeline-entry {
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
background: $white-light;
.timeline-entry-inner {
position: relative;
......
.tooltip-inner {
font-size: $tooltip-font-size;
border-radius: $border-radius-default;
line-height: 16px;
font-weight: $gl-font-weight-normal;
padding: $gl-btn-padding;
}
/*
* Layout
*/
$grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
......@@ -202,6 +203,11 @@ $md-area-border: #ddd;
$code_font_size: 12px;
$code_line_height: 1.6;
/*
* Tooltips
*/
$tooltip-font-size: 12px;
/*
* Padding
*/
......@@ -262,7 +268,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
$user-mention-color: #2fa0bb;
$user-mention-bg: rgba($blue-500, 0.044);
$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
......@@ -316,6 +323,7 @@ $diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
$diff-view-modes-border: #c1c1c1;
$diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
......@@ -699,3 +707,15 @@ Project Templates Icons
$rails: #c00;
$node: #353535;
$java: #70ad51;
/*
Issuable warning
*/
$issuable-warning-size: 24px;
$issuable-warning-icon-margin: 4px;
/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
.edit-cluster-form {
.clipboard-addon {
background-color: $white-light;
}
.alert-block {
margin-bottom: 20px;
}
}
......@@ -297,6 +297,7 @@
.drag-track {
display: block;
position: absolute;
top: 0;
left: 12px;
height: 10px;
width: 276px;
......@@ -547,16 +548,23 @@
}
.diff-notes-collapse {
width: 19px;
height: 19px;
width: 24px;
height: 24px;
border-radius: 50%;
padding: 0;
transition: transform .1s ease-out;
z-index: 100;
.collapse-icon {
height: 50%;
width: 100%;
}
svg {
vertical-align: text-top;
vertical-align: middle;
}
.collapse-icon,
path {
fill: $white-light;
}
......@@ -644,3 +652,157 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
// double jagged line divider
.discussion-notes + .discussion-notes::before,
.discussion-notes + .discussion-form::before {
content: '';
position: relative;
display: block;
width: 100%;
height: 10px;
background-color: $white-light;
background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
background-position: 5px 5px,0 5px,0 5px,5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
.notes {
position: relative;
}
.diff-notes-collapse {
position: absolute;
left: -12px;
}
}
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
.notes.active {
.diff-file .note-container > .new-note,
.note-container .discussion-notes {
// Override our margin and border (set for diff tab)
// when user is on the discussion tab for MR
margin-left: inherit;
border-left: inherit;
}
}
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
.frame.click-to-comment {
position: relative;
cursor: url(icon_image_comment.svg)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
position: absolute;
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
height: (1px * $image-comment-cursor-top-offset);
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
svg {
width: 100%;
height: 100%;
}
&:focus {
outline: none;
}
}
}
.frame .badge,
.image-diff-avatar-link .badge,
.notes > .badge {
position: absolute;
background-color: $blue-400;
color: $white-light;
border: $white-light 1px solid;
min-height: $gl-padding;
padding: 5px 8px;
border-radius: 12px;
&:focus {
outline: none;
}
}
.frame .badge,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate3d(-50%, -50%, 0);
}
.image-comment-badge {
@include btn-comment-icon;
position: absolute;
&.inverted {
border-color: $white-light;
}
}
.image-diff-avatar-link {
position: relative;
.badge,
.image-comment-badge {
top: 25px;
right: 8px;
}
}
.notes > .badge {
display: none;
left: -13px;
}
.discussion-notes {
min-height: 35px;
&:first-child {
// First child does not have the jagged borders
min-height: 25px;
}
&.collapsed {
background-color: $white-light;
.diff-notes-collapse,
.note,
.discussion-reply-holder, {
display: none;
}
.notes > .badge {
display: block;
}
}
}
.discussion-body .image .frame {
position: relative;
}
......@@ -207,10 +207,13 @@
}
.prometheus-state {
margin-top: 10px;
max-width: 430px;
margin: 10px auto;
text-align: center;
.state-button-section {
margin-top: 10px;
.state-svg {
max-width: 80vw;
margin: 0 auto;
}
}
......@@ -288,9 +291,15 @@
fill: $black;
}
.tick > text {
.tick {
> line {
stroke: $gray-darker;
}
> text {
font-size: 12px;
}
}
.text-metric-title {
font-size: 12px;
......
......@@ -5,27 +5,29 @@
margin-right: auto;
}
.is-confidential {
.issuable-warning-icon {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
&:first-of-type {
margin-right: $issuable-warning-icon-margin;
}
}
.is-not-confidential {
.sidebar-item-icon {
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
.confidentiality {
.is-not-confidential {
margin: auto;
}
.is-confidential {
margin: auto;
&.is-active {
color: $orange-600;
background-color: $orange-50;
}
}
......
......@@ -101,7 +101,7 @@
}
}
.confidential-issue-warning {
.issuable-note-warning {
color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
......@@ -110,37 +110,64 @@
padding: 3px 12px;
margin: auto;
align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
}
}
.disabled-comment .issuable-note-warning {
border: none;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
}
.confidential-value {
.sidebar-item-value {
.fa {
background-color: inherit;
}
}
.confidential-warning-message {
.sidebar-item-warning-message {
line-height: 1.5;
padding: 16px;
.confidential-warning-message-actions {
.text {
color: $text-color;
}
.sidebar-item-warning-message-actions {
display: flex;
button {
.btn {
flex-grow: 1;
}
}
}
.confidential-issue-warning + .md-area {
.issuable-note-warning + .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
.discussion-form-container {
padding: $gl-padding-top $gl-padding $gl-padding;
}
.discussion-notes .disabled-comment {
padding: 6px 0;
}
......
......@@ -650,29 +650,12 @@ ul.notes {
}
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
margin-top: -2px;
border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $blue-500;
&:hover {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
}
&:active {
outline: 0;
}
}
.discussion-body,
......@@ -703,6 +686,12 @@ ul.notes {
color: $note-disabled-comment-color;
padding: 90px 0;
&.discussion-locked {
border: none;
background-color: $white-light;
}
a {
color: $gl-link-color;
}
......
......@@ -108,6 +108,15 @@
}
}
.subkeys-list {
@include basic-list;
li {
padding: 3px 0;
border: none;
}
}
.key-list-item {
.key-list-item-info {
@media (min-width: $screen-sm-min) {
......@@ -392,11 +401,11 @@ table.u2f-registrations {
}
}
.gpg-email-badge {
.email-badge {
display: inline;
margin-right: $gl-padding / 2;
.gpg-email-badge-email {
.email-badge-email {
display: inline;
margin-right: $gl-padding / 4;
}
......
......@@ -499,22 +499,17 @@ a.deploy-project-label {
}
}
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.fork-thumbnail {
height: 200px;
width: calc((100% / 2) - #{$gl-padding * 2});
.fork-thumbnail {
border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
@media (min-width: $screen-md-min) {
width: calc((100% / 4) - #{$gl-padding * 2});
}
@media (min-width: $screen-lg-min) {
width: calc((100% / 5) - #{$gl-padding * 2});
}
&:hover:not(.disabled),
&.forked {
......@@ -522,18 +517,11 @@ a.deploy-project-label {
border-color: $row-hover-border;
}
.no-avatar {
width: 100px;
height: 100px;
background-color: $gray-light;
border: 1px solid $white-normal;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
color: $white-normal;
}
.avatar-container,
.identicon {
float: none;
margin-left: auto;
margin-right: auto;
}
a {
......@@ -541,28 +529,23 @@ a.deploy-project-label {
width: 100%;
height: 100%;
padding-top: $gl-padding;
color: $gl-text-color;
text-decoration: none;
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
}
.caption {
min-height: 30px;
padding: $gl-padding 0;
}
}
.fork-thumbnail-container {
display: flex;
flex-wrap: wrap;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
img {
border-radius: 50%;
max-width: 100px;
}
}
> h5 {
width: 100%;
}
}
......
......@@ -169,6 +169,14 @@
}
}
.tree-item-file-external-link {
margin-right: 4px;
span {
text-decoration: inherit;
}
}
.tree_commit {
max-width: 320px;
......
......@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email
email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute
success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format|
if success
......
......@@ -85,10 +85,19 @@ class ApplicationController < ActionController::Base
super
payload[:remote_ip] = request.remote_ip
if current_user.present?
payload[:user_id] = current_user.id
payload[:username] = current_user.username
logged_user = auth_user
if logged_user.present?
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end
end
# Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
return try(:authenticated_user)
end
# This filter handles both private tokens and personal access tokens
......
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.
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.
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.
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.
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.
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.
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.
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.
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.
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