Commit 93c0729a authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into add-ci_variables-environment_scope

* upstream/master: (226 commits)
  Polish sidebar toggle
  Add changelog entry
  Fix optional args for POST :id/variables
  Update CHANGELOG.md for 9.3.1
  Bump bootsnap to 1.1.1
  Add explicit message when no runners on admin
  Fix endpoint not being update correctly
  Remove unused Gitlab::Git::Commit#to_diff argument
  Drop GFM support for the title of Milestone/MergeRequest in template
  Handle Promise rejections in mr_widget_pipeline_spec.js
  Handle missing pipeline in merge request widget
  Store merge request ref_fetched status in the database
  Tag a spec as :nested_groups since it fails on MySQL
  Bump premailer-rails gem to 1.9.7 and its dependencies to prevent network retrieval of assets
  Truncate long job names in environment view; wrap author to next line
  Replaces 'dashboard/merge_requests' spinach with rspec
  Update GITLAB_SHELL_VERSION to 5.0.6
  Fix click not being able to find the current element to use trigger('click') instead
  make accepting mrs more prominent
  Remove last references to job for pipeline charts
  ...
parents 10e732d2 eacce60b
......@@ -461,6 +461,7 @@ karma:
- coverage-javascript/
codeclimate:
<<: *except-docs
before_script: []
image: docker:latest
stage: test
......
This diff is collapsed.
......@@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone.
Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
......
......@@ -2,7 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
gem 'bootsnap', '~> 1.1'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
......@@ -123,6 +123,7 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'bootstrap_form', '~> 2.7.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......@@ -256,7 +257,7 @@ gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
gem 'premailer-rails', '~> 1.9.7'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
......@@ -384,7 +385,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.8.0'
gem 'gitaly', '~> 0.9.0'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -83,11 +83,12 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
bootsnap (1.1.1)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (3.6.1)
browser (2.2.0)
builder (3.2.3)
......@@ -138,7 +139,7 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
css_parser (1.4.1)
css_parser (1.5.0)
addressable
d3_rails (3.5.11)
railties (>= 3.1.0)
......@@ -277,7 +278,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.8.0)
gitaly (0.9.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -353,7 +354,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.2.5)
grpc (1.4.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......@@ -591,10 +592,11 @@ GEM
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
powerpack (0.1.1)
premailer (1.8.6)
css_parser (>= 1.3.6)
premailer (1.10.4)
addressable
css_parser (>= 1.4.10)
htmlentities (>= 4.0.0)
premailer-rails (1.9.2)
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5)
......@@ -928,8 +930,9 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootsnap (~> 1.1)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
bullet (~> 5.5.0)
......@@ -977,7 +980,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.8.0)
gitaly (~> 0.9.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
......@@ -1047,7 +1050,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta5)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
......
This diff is collapsed.
......@@ -17,7 +17,7 @@ export default {
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
if (this.title.trim() === '') return Promise.resolve();
this.error = false;
......@@ -29,7 +29,10 @@ export default {
assignees: [],
});
this.list.newIssue(issue)
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
......@@ -47,9 +50,6 @@ export default {
// Show error message
this.error = true;
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
},
cancel() {
this.title = '';
......
......@@ -112,8 +112,7 @@ class List {
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
})
.then(() => {
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
......
......@@ -55,6 +55,7 @@ import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
(function() {
var Dispatcher;
......@@ -120,6 +121,9 @@ import initSettingsPanels from './settings_panels';
}
switch (page) {
case 'profiles:preferences:show':
initExperimentalFlags();
break;
case 'sessions:new':
new UsernameValidator();
new ActiveTabMemoizer();
......
......@@ -2,6 +2,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,6 +13,10 @@ export default {
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
......@@ -33,8 +38,6 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
......@@ -53,11 +56,11 @@ export default {
class="btn-group"
role="group">
<button
v-tooltip
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the external url link in environments table.
*/
......@@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Open';
......@@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn external-url has-tooltip"
v-tooltip
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
......
......@@ -403,6 +403,14 @@ export default {
return '';
},
displayEnvironmentActions() {
return this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.hasStopAction ||
this.canRetry;
},
/**
* Constructs folder URL based on the current location and the folder id.
*
......@@ -535,9 +543,12 @@ export default {
</span>
</div>
<div class="table-section section-30 table-button-footer" role="gridcell">
<div
v-if="!model.isFolder"
v-if="!model.isFolder && displayEnvironmentActions"
class="table-section section-30 table-button-footer"
role="gridcell">
<div
class="btn-group table-action-buttons"
role="group">
......
......@@ -2,6 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
monitoringUrl: {
......@@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
......@@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn monitoring-url hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
......
......@@ -5,6 +5,7 @@
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
......@@ -46,8 +51,9 @@ export default {
</script>
<template>
<button
v-tooltip
type="button"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
class="btn stop-env-link hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
......
......@@ -4,6 +4,7 @@
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
......@@ -29,7 +34,8 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn terminal-button hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
......
import Cookies from 'js-cookie';
export default () => {
$('.js-experiment-feature-toggle').on('change', (e) => {
const el = e.target;
Cookies.set(el.name, el.value, {
expires: 365 * 10,
});
});
};
......@@ -40,6 +40,10 @@ class FilteredSearchManager {
return [];
})
.then((searches) => {
if (!searches) {
return;
}
// Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones
const resultantSearches = this.recentSearchesStore.setRecentSearches(
......@@ -487,6 +491,7 @@ class FilteredSearchManager {
}
searchState(e) {
e.preventDefault();
const target = e.currentTarget;
// remove focus outline after click
target.blur();
......
......@@ -47,8 +47,8 @@ export default class GroupsStore {
// Map groups to an object
groups.map((group) => {
mappedGroups[group.id] = group;
mappedGroups[group.id].subGroups = {};
mappedGroups[`id${group.id}`] = group;
mappedGroups[`id${group.id}`].subGroups = {};
return group;
});
......@@ -56,26 +56,27 @@ export default class GroupsStore {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[currentGroup.parentId];
const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
if (findParentGroup) {
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[currentGroup.id] = currentGroup;
tree[`id${currentGroup.id}`] = currentGroup;
} else {
// Means the groups hast no direct parent.
// Save for later processing, we will add them to its corresponding base group
// No parent found. We save it for later processing
orphans.push(currentGroup);
// Add to tree to preserve original order
tree[`id${currentGroup.id}`] = currentGroup;
}
} else {
// If the group is at the root level, add it to first level elements array.
tree[currentGroup.id] = currentGroup;
// If the group is at the top level, add it to first level elements array.
tree[`id${currentGroup.id}`] = currentGroup;
}
return key;
});
// Hopefully this array will be empty for most cases
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
......@@ -83,11 +84,23 @@ export default class GroupsStore {
Object.keys(tree).map((key) => {
const group = tree[key];
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
if (
group &&
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
// Make sure the currently selected orphan is not the same as the group
// we are checking here otherwise it will end up in an infinite loop
currentOrphan.id !== group.id
) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
// Delete if group was put at the top level. If not the group will be displayed twice.
if (tree[`id${currentOrphan.id}`]) {
delete tree[`id${currentOrphan.id}`];
}
}
return key;
......@@ -95,7 +108,8 @@ export default class GroupsStore {
if (!found) {
currentOrphan.isOrphan = true;
tree[currentOrphan.id] = currentOrphan;
tree[`id${currentOrphan.id}`] = currentOrphan;
}
return orphan;
......@@ -140,7 +154,7 @@ export default class GroupsStore {
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, group.id);
Vue.delete(collection, `id${group.id}`);
}
// eslint-disable-next-line class-methods-use-this
......
......@@ -204,13 +204,7 @@ export default {
method: 'getData',
successCallback: (res) => {
const data = res.json();
const shouldUpdate = this.store.stateShouldUpdate(data);
this.store.updateState(data);
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
this.store.formState.lockedWarningVisible = true;
}
},
errorCallback(err) {
throw new Error(err);
......
......@@ -47,7 +47,8 @@
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="updateIssuable">
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable">
</textarea>
</markdown-field>
</div>
......
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
props: {
formState: {
type: Object,
......@@ -71,9 +71,9 @@
data-placeholder="Move to a different project" />
</div>
<span
v-tooltip
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
<i
class="fa fa-question-circle"
aria-hidden="true">
......
......@@ -26,6 +26,7 @@
placeholder="Issue title"
aria-label="Issue title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable" />
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
</fieldset>
</template>
......@@ -12,6 +12,10 @@ export default class Store {
}
updateState(data) {
if (this.stateShouldUpdate(data)) {
this.formState.lockedWarningVisible = true;
}
this.state.titleHtml = data.title;
this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description;
......@@ -23,10 +27,8 @@ export default class Store {
}
stateShouldUpdate(data) {
return {
title: this.state.titleText !== data.title_text,
description: this.state.descriptionText !== data.description_text,
};
return this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text;
}
setFormState(state) {
......
......@@ -86,18 +86,25 @@
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
var fixedTabs = document.querySelector('.js-tabs-affix');
var fixedNav = document.querySelector('.navbar-gitlab');
var adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
// scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
window.scrollBy(0, adjustment);
}
} else {
// only adjust for fixedTabs when not targeting user-generated content
var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) {
window.scrollBy(0, -fixedTabs.offsetHeight);
adjustment -= fixedTabs.offsetHeight;
}
window.scrollBy(0, adjustment);
}
};
......
This diff is collapsed.
This diff is collapsed.
......@@ -299,9 +299,10 @@ $(function () {
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function () {
$('.header-content .title').toggle();
$('.header-content .title, .header-content .navbar-sub-nav').toggle();
$('.header-content .header-logo').toggle();
$('.header-content .navbar-collapse').toggle();
$('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff
......
......@@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
scrollToElement(container) {
if (location.hash) {
const offset = -$('.js-tabs-affix').outerHeight();
const offset = 0 - (
$('.navbar-gitlab').outerHeight() +
$('.js-tabs-affix').outerHeight()
);
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
......@@ -291,7 +294,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
const anchor = hash && $container.find(`.note[id="${hash}"]`);
if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
......@@ -301,6 +304,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
forceShow: true,
});
anchor[0].scrollIntoView();
window.gl.utils.handleLocationHash();
// We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
......
This diff is collapsed.
......@@ -3,7 +3,7 @@
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -28,12 +28,12 @@ export default {
required: false,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
......@@ -58,7 +58,6 @@ export default {
makeRequest() {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
},
},
......@@ -67,6 +66,7 @@ export default {
<template>
<button
v-tooltip
type="button"
@click="onClick"
:class="buttonClass"
......@@ -74,7 +74,6 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
ref="tooltip"
:disabled="isLoading">
<i
:class="iconClass"
......
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
......@@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
......@@ -46,12 +46,11 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i
......
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
......@@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
......@@ -42,13 +42,12 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">
......
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
......@@ -34,9 +34,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
jobComponent,
......@@ -53,12 +53,12 @@
<template>
<div>
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
:title="tooltipText">
<job-name-component
:name="job.name"
......
......@@ -2,7 +2,7 @@
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
......@@ -54,9 +54,9 @@
jobNameComponent,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
tooltipText() {
......@@ -77,12 +77,11 @@
<template>
<div>
<a
v-tooltip
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
......@@ -93,10 +92,9 @@
<div
v-else
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
......
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,9 +12,9 @@ export default {
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
user() {
return this.pipeline.user;
......@@ -45,16 +45,16 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
v-tooltip
class="js-pipeline-url-latest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
......
......@@ -4,6 +4,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,6 +13,9 @@
required: true,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
......@@ -25,8 +29,6 @@
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
......@@ -43,13 +45,13 @@
<template>
<div class="btn-group">
<button
v-tooltip
type="button"
class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i
......
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -8,9 +8,9 @@
required: true,
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
......@@ -18,12 +18,12 @@
class="btn-group"
role="group">
<button
v-tooltip
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
aria-label="Artifacts">
<i
class="fa fa-download"
aria-hidden="true">
......
......@@ -16,7 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -32,15 +32,14 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
data() {
return {
isLoading: false,
dropdownContent: '',
endpoint: this.stage.dropdown_path,
};
},
......@@ -73,7 +72,7 @@ export default {
},
fetchJobs() {
this.$http.get(this.endpoint)
this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.dropdownContent = response.json().html;
this.isLoading = false;
......@@ -132,7 +131,7 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
v-tooltip
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
......
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
......@@ -16,9 +16,11 @@
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
data() {
return {
iconTimerSvg,
......@@ -81,7 +83,7 @@
</i>
<time
ref="tooltip"
v-tooltip
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">
......
export default {
EMPTY: 'empty',
LOADING: 'loading',
LIST: 'list',
};
import PrometheusMetrics from './prometheus_metrics';
$(() => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
});
import PANEL_STATE from './constants';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
this.backOffRequestCounter = 0;
this.$wrapper = $(wrapperSelector);
this.$monitoredMetricsPanel = this.$wrapper.find('.js-panel-monitored-metrics');
this.$monitoredMetricsCount = this.$monitoredMetricsPanel.find('.js-monitored-count');
this.$monitoredMetricsLoading = this.$monitoredMetricsPanel.find('.js-loading-metrics');
this.$monitoredMetricsEmpty = this.$monitoredMetricsPanel.find('.js-empty-metrics');
this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list');
this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars');
this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle');
this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count');
this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list');
this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics');
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
} else {
$toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down');
}
}
showMonitoringMetricsPanelState(stateName) {
switch (stateName) {
case PANEL_STATE.LOADING:
this.$monitoredMetricsLoading.removeClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
case PANEL_STATE.LIST:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.removeClass('hidden');
break;
default:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.removeClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
}
}
populateActiveMetrics(metrics) {
let totalMonitoredMetrics = 0;
let totalMissingEnvVarMetrics = 0;
metrics.forEach((metric) => {
this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`);
totalMonitoredMetrics += metric.active_metrics;
if (metric.metrics_missing_requirements > 0) {
this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`);
totalMissingEnvVarMetrics += 1;
}
});
this.$monitoredMetricsCount.text(totalMonitoredMetrics);
this.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
if (totalMissingEnvVarMetrics > 0) {
this.$missingEnvVarPanel.removeClass('hidden');
this.$missingEnvVarPanel.find('.flash-container').off('click');
this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics);
}
}
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
gl.utils.backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint)
.done((res) => {
if (res && res.success) {
stop(res);
} else {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
}
})
.fail(stop);
})
.then((res) => {
if (res && res.data && res.data.length) {
this.populateActiveMetrics(res.data);
} else {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
}
})
.catch(() => {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
});
}
}
......@@ -10,8 +10,6 @@ import Cookies from 'js-cookie';
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners();
......@@ -215,7 +213,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
const $navHeight = this.$navGitlab.outerHeight();
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);
......
......@@ -4,7 +4,7 @@ function expandSectionParent($section, $content) {
}
function expandSection($section) {
$section.find('.js-settings-toggle').text('Close');
$section.find('.js-settings-toggle').text('Collapse');
const $content = $section.find('.settings-content');
$content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
......
......@@ -14,6 +14,11 @@ export default {
type: Boolean,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
assigneeTitle() {
......@@ -36,6 +41,19 @@ export default {
>
Edit
</a>
<a
v-if="showToggle"
aria-label="Toggle sidebar"
class="gutter-toggle pull-right js-sidebar-toggle"
href="#"
role="button"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
/>
</a>
</div>
`,
};
......@@ -64,6 +64,7 @@ export default {
},
beforeMount() {
this.field = this.$el.dataset.field;
this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
},
template: `
<div>
......@@ -71,6 +72,7 @@ export default {
:number-of-assignees="store.assignees.length"
:loading="loading || store.isFetching.assignees"
:editable="store.editable"
:show-toggle="!signedIn"
/>
<assignees
v-if="!store.isFetching.assignees"
......
......@@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>";
return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>";
};
UsersSelect.prototype.formatSelection = function(user) {
......
......@@ -17,6 +17,9 @@ export default {
return hasCI && !ciStatus;
},
hasPipeline() {
return Object.keys(this.mr.pipeline || {}).length > 0;
},
svg() {
return statusIconEntityMap.icon_status_failed;
},
......@@ -30,7 +33,11 @@ export default {
template: `
<div class="mr-widget-heading">
<div class="ci-widget">
<template v-if="hasCIError">
<template v-if="!hasPipeline">
<i class="fa fa-spinner fa-spin append-right-10" aria-hidden="true"></i>
Waiting for pipeline...
</template>
<template v-else-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
<span class="js-icon-link icon-link">
<span
......
......@@ -2,7 +2,7 @@
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
......@@ -47,9 +47,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
ciIconBadge,
......@@ -90,10 +90,10 @@ export default {
<template v-if="user">
<a
v-tooltip
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
class="js-user-link commit-committer-link">
<user-avatar-image
:img-src="user.avatar_url"
......
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
directives: {
tooltip,
},
components: {
toolbarButton,
},
......@@ -94,13 +94,13 @@
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
type="button">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">
......
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
......@@ -29,6 +26,9 @@
default: false,
},
},
directives: {
tooltip,
},
computed: {
iconClass() {
return `fa-${this.icon}`;
......@@ -39,10 +39,10 @@
<template>
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
......
<script>
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
......@@ -28,19 +28,21 @@ export default {
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
<time
v-tooltip
:class="cssClass"
class="js-vue-timeago"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
data-container="body">
{{timeFormated(time)}}
</time>
</template>
......@@ -16,11 +16,10 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
......@@ -53,6 +52,9 @@ export default {
default: 'top',
},
},
directives: {
tooltip,
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
......@@ -72,6 +74,7 @@ export default {
<template>
<img
v-tooltip
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imageSource"
......@@ -81,6 +84,5 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>
export default {
bind(el) {
$(el).tooltip();
},
componentUpdated(el) {
$(el).tooltip('fixTitle');
},
unbind(el) {
$(el).tooltip('destroy');
},
};
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
};
......@@ -395,6 +395,11 @@
.dropdown-menu-align-right {
left: auto;
right: 0;
margin-top: -5px;
@media (max-width: $screen-xs-max) {
left: 0;
}
}
.dropdown-menu-selectable {
......
......@@ -12,6 +12,12 @@
&.readme-holder {
margin: $gl-padding 0;
&.limited-width-container .file-content {
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
}
}
table {
......@@ -123,7 +129,7 @@
}
/**
* Annotate file
* Blame file
*/
&.blame {
table {
......
......@@ -34,6 +34,8 @@ header {
top: 0;
left: 0;
right: 0;
color: $gl-text-color-secondary;
border-radius: 0;
@media (max-width: $screen-xs-min) {
padding: 0 16px;
......@@ -59,7 +61,7 @@ header {
padding: 0;
.nav > li > a {
color: $gl-text-color-secondary;
color: currentColor;
font-size: 18px;
padding: 0;
margin: (($header-height - 28) / 2) 3px;
......@@ -84,7 +86,7 @@ header {
&:hover,
&:focus,
&:active {
background-color: $gray-light;
background-color: transparent;
color: $gl-text-color;
svg {
......@@ -96,13 +98,19 @@ header {
font-size: 14px;
}
.fa-chevron-down {
position: relative;
top: -3px;
font-size: 10px;
}
svg {
position: relative;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
width: 23px;
fill: $gl-text-color-secondary;
fill: currentColor;
}
}
......@@ -225,7 +233,7 @@ header {
}
a {
color: $gl-text-color;
color: currentColor;
&:hover {
text-decoration: underline;
......@@ -346,6 +354,8 @@ header {
width: auto;
min-width: 140px;
margin-top: -5px;
color: $gl-text-color;
left: auto;
.current-user {
padding: 5px 18px;
......
......@@ -53,7 +53,7 @@ body {
}
&.limit-container-width-sm {
max-width: 790px;
max-width: $limited-layout-width-sm;
}
}
......
......@@ -45,8 +45,7 @@
li {
display: flex;
a,
.btn-link {
a {
padding: $gl-btn-padding;
padding-bottom: 11px;
font-size: 14px;
......@@ -68,29 +67,7 @@
}
}
.btn-link {
padding-top: 16px;
padding-left: 15px;
padding-right: 15px;
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
&:hover,
&:active,
&:focus {
background-color: transparent;
}
&:active {
outline: 0;
box-shadow: none;
}
}
&.active a,
&.active .btn-link {
&.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black;
font-weight: 600;
......
......@@ -44,6 +44,10 @@
&:target,
&.target {
background: $line-target-blue;
&.system-note .note-body .note-text.system-note-commit-list::after {
background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%);
}
}
.avatar {
......
......@@ -74,6 +74,10 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
$purple-700: #4a2192;
$purple-800: #2c0a5c;
$purple-900: #380d75;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
......@@ -161,6 +165,7 @@ $progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
$border-radius-default: 3px;
......@@ -323,6 +328,7 @@ $note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
$note-line2-border: #ddd;
$note-icon-gutter-width: 55px;
/*
......
@import "framework/variables";
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
header.navbar-gitlab-new {
color: $white-light;
background-color: $purple-900;
border-bottom: 0;
.header-content {
padding-left: 0;
.title-container {
padding-top: 0;
overflow: visible;
}
.title {
display: block;
height: 100%;
padding-right: 0;
color: currentColor;
> a {
display: flex;
align-items: center;
height: 100%;
padding-top: 3px;
padding-right: $gl-padding;
padding-left: $gl-padding;
margin-left: -$gl-padding;
border-bottom: 3px solid transparent;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
padding-left: $gl-padding;
}
svg {
margin-top: -3px;
@media (min-width: $screen-sm-min) {
margin-right: 10px;
}
}
&:hover,
&:focus {
color: currentColor;
text-decoration: none;
border-bottom-color: $white-light;
}
}
}
.dropdown.open {
> a {
border-bottom-color: $white-light;
}
}
.dropdown-menu {
margin-top: 4px;
min-width: 130px;
@media (max-width: $screen-xs-max) {
left: auto;
right: 0;
}
}
}
.navbar-collapse {
padding-left: 0;
color: $white-light;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
margin-left: -$gl-padding;
margin-right: -10px;
}
.dropdown-bold-header {
color: initial;
}
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
margin-left: 0;
min-width: 100%;
}
}
}
}
.container-fluid {
.navbar-toggle {
min-width: 45px;
padding: 6px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
border-left: 1px solid lighten($purple-700, 10%);
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
}
.navbar-nav {
@media (max-width: $screen-xs-max) {
display: flex;
padding-right: 10px;
}
li {
.badge {
box-shadow: none;
}
}
}
.nav > li {
&.header-user {
@media (max-width: $screen-xs-max) {
padding-left: 10px;
}
}
> a {
background: none;
opacity: .9;
will-change: opacity;
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
&:hover,
&:focus {
color: $white-light;
opacity: 1;
> svg {
fill: $white-light;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $white-light;
}
}
}
}
}
}
}
.navbar-sub-nav {
display: flex;
margin-bottom: 0;
color: $white-light;
> li {
&.active > a,
a:hover,
a:focus {
border-bottom-color: $white-light;
text-decoration: none;
outline: 0;
opacity: 1;
}
> a {
display: block;
padding: 16px 10px 13px;
font-size: 13px;
color: currentColor;
border-bottom: 3px solid transparent;
opacity: .9;
will-change: opacity;
@media (min-width: $screen-sm-min) {
padding: 15px $gl-padding 12px;
font-size: 14px;
}
}
}
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
}
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
margin-top: 4px;
}
.search {
form {
border-color: $purple-800;
&:hover {
border-color: rgba($white-light, .6);
box-shadow: none;
}
}
&.search-active form {
border-color: $white-light;
}
form,
.search-input {
background-color: $purple-700;
}
.search-input {
color: $white-light;
}
.search-input::placeholder {
color: rgba($white-light, .6);
}
.location-badge {
font-size: 12px;
color: rgba($white-light, .6);
background-color: $purple-800;
transition: color 0.15s;
will-change: color;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: rgba($white-light, .6);
}
}
&.search-active {
.location-badge {
color: $white-light;
background-color: $purple-800;
}
.search-input-wrap {
.search-icon {
color: rgba($white-light, .6);
}
.clear-icon {
color: $white-light;
}
}
}
}
......@@ -83,6 +83,7 @@
.avatar {
float: none;
margin-right: 0;
}
}
......
......@@ -11,7 +11,9 @@
.commit-box,
.info-well,
.commit-ci-menu,
.files-changed {
.files-changed,
.limited-header-width,
.limited-width-notes {
@extend .fixed-width-container;
}
......@@ -226,6 +228,12 @@
padding-top: 10px;
}
&:not(.issue-boards-sidebar):not([data-signed-in]) {
.issuable-sidebar-header {
display: none;
}
}
.assign-yourself .btn-link {
padding-left: 0;
}
......@@ -247,6 +255,10 @@
border-left: 1px solid $border-gray-normal;
}
.title .gutter-toggle {
margin-top: 0;
}
.assignee .avatar {
float: left;
margin-right: 10px;
......
......@@ -136,10 +136,6 @@
width: 250px;
}
@media (min-width: $screen-md-min) {
width: 350px;
}
&.input-short {
@media (min-width: $screen-md-min) {
width: 170px;
......
......@@ -419,7 +419,7 @@
.commit {
margin: 0;
padding: 10px 0;
padding: 10px;
list-style: none;
&:hover {
......
......@@ -148,8 +148,20 @@
padding: 6px 0;
}
.notes-form > li {
.notes.notes-form > li.timeline-entry {
@include notes-media('max', $screen-sm-max) {
padding: 0;
}
.timeline-content {
@include notes-media('max', $screen-sm-max) {
margin: 0;
}
}
.timeline-entry-inner {
border: 0;
}
}
.note-edit-form {
......
......@@ -14,16 +14,6 @@ ul.notes {
margin: 0;
padding: 0;
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
@include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
}
.note-created-ago,
.note-updated-at {
white-space: nowrap;
......@@ -46,15 +36,47 @@ ul.notes {
}
}
> li {
padding: $gl-padding $gl-btn-padding;
> li { // .timeline-entry
padding: 0;
display: block;
position: relative;
border-bottom: 0;
@include notes-media('min', $screen-sm-min) {
padding-left: $note-icon-gutter-width;
}
.timeline-entry-inner {
padding: $gl-padding $gl-btn-padding;
border-bottom: 1px solid $white-normal;
}
&:last-child {
// Override `.timeline > li:last-child { border-bottom: none; }`
&:target,
&.target {
border-bottom: 1px solid $white-normal;
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: -1px;
}
.timeline-entry-inner {
border-bottom: 0;
}
}
.timeline-icon {
@include notes-media('min', $screen-sm-min) {
margin-left: -$note-icon-gutter-width;
}
}
.timeline-content {
margin-left: $note-icon-gutter-width;
@include notes-media('min', $screen-sm-min) {
margin-left: 0;
}
}
&.being-posted {
......@@ -73,7 +95,7 @@ ul.notes {
}
&.note-discussion {
&.timeline-entry {
.timeline-entry-inner {
padding: $gl-padding 10px;
}
}
......@@ -152,13 +174,8 @@ ul.notes {
.system-note {
font-size: 14px;
padding-left: 0;
clear: both;
@include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
.note-header-info {
padding-bottom: 0;
}
......@@ -192,13 +209,16 @@ ul.notes {
.timeline-icon {
float: left;
@include notes-media('min', $screen-sm-min) {
margin-left: 0;
width: auto;
}
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 2px;
margin-top: 2px;
}
}
......@@ -250,7 +270,7 @@ ul.notes {
&::after {
content: '';
width: 100%;
height: 67px;
height: 70px;
position: absolute;
left: 0;
bottom: 0;
......@@ -639,15 +659,12 @@ ul.notes {
.discussion-body,
.diff-file {
.notes .note {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
&.system-note {
padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
border-bottom: none;
}
}
}
......
......@@ -133,7 +133,7 @@
overflow: hidden;
display: inline-block;
white-space: nowrap;
vertical-align: top;
vertical-align: middle;
text-overflow: ellipsis;
}
......
......@@ -380,7 +380,7 @@ a.deploy-project-label {
padding: 0;
background: transparent;
border: none;
line-height: 36px;
line-height: 34px;
margin: 0;
> li + li::before {
......
......@@ -126,3 +126,66 @@
margin-left: 5px;
}
}
.prometheus-metrics-monitoring {
.panel {
.panel-toggle {
width: 14px;
}
.badge {
font-size: inherit;
}
.panel-heading .badge-count {
color: $white-light;
background: $common-gray-dark;
}
.panel-body {
padding: 0;
}
.flash-container {
margin-bottom: 0;
cursor: default;
.flash-notice {
border-radius: 0;
}
}
}
.loading-metrics,
.empty-metrics {
padding: 30px 10px;
p,
.btn {
margin-top: 10px;
margin-bottom: 0;
}
}
.loading-metrics .metrics-load-spinner {
color: $loading-color;
}
.metrics-list {
margin-bottom: 0;
li {
padding: $gl-padding;
.badge {
margin-left: 5px;
background: $badge-bg;
}
}
/* Ensure we don't add border if there's only single li */
li + li {
border-top: 1px solid $border-color;
}
}
}
.tree-holder {
> .nav-block {
margin: 11px 0;
.nav-block {
margin: 10px 0;
@media (min-width: $screen-sm-min) {
display: flex;
.tree-ref-container {
flex: 1;
}
.tree-controls {
text-align: right;
.btn-group {
margin-left: 10px;
}
}
.tree-ref-holder {
float: left;
margin-right: 15px;
}
.repo-breadcrumb {
li:last-of-type {
position: relative;
}
}
.add-to-tree-dropdown {
position: absolute;
left: 18px;
}
}
}
@media (max-width: $screen-xs-max) {
.repo-breadcrumb {
margin-top: 10px;
position: relative;
.dropdown-menu {
min-width: 100%;
width: 100%;
left: inherit;
right: 0;
}
}
.add-to-tree-dropdown {
position: absolute;
left: 0;
right: 0;
}
.tree-controls {
margin-bottom: 10px;
.btn,
.dropdown,
.btn-group {
width: 100%;
}
.btn {
margin: 10px 0 0;
}
}
}
.file-finder {
......@@ -131,11 +197,6 @@
}
}
.tree-ref-holder {
float: left;
margin-right: 15px;
}
.blob-commit-info {
list-style: none;
margin: 0;
......@@ -159,16 +220,6 @@
color: $md-link-color;
}
.tree-controls {
float: right;
position: relative;
z-index: 2;
.project-action-button {
margin-left: $btn-side-margin;
}
}
.repo-charts {
.sub-header {
margin: 20px 0;
......
......@@ -54,7 +54,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def block
if user.block
if update_user { |user| user.block }
redirect_back_or_admin_user(notice: "Successfully blocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not blocked")
......@@ -64,7 +64,7 @@ class Admin::UsersController < Admin::ApplicationController
def unblock
if user.ldap_blocked?
redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab")
elsif user.activate
elsif update_user { |user| user.activate }
redirect_back_or_admin_user(notice: "Successfully unblocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
......@@ -72,7 +72,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def unlock
if user.unlock_access!
if update_user { |user| user.unlock_access! }
redirect_back_or_admin_user(alert: "Successfully unlocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unlocked")
......@@ -80,7 +80,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def confirm
if user.confirm
if update_user { |user| user.confirm }
redirect_back_or_admin_user(notice: "Successfully confirmed")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not confirmed")
......@@ -88,7 +88,8 @@ class Admin::UsersController < Admin::ApplicationController
end
def disable_two_factor
user.disable_two_factor!
update_user { |user| user.disable_two_factor! }
redirect_to admin_user_path(user),
notice: 'Two-factor Authentication has been disabled for this user'
end
......@@ -124,15 +125,18 @@ class Admin::UsersController < Admin::ApplicationController
end
respond_to do |format|
result = Users::UpdateService.new(user, user_params_with_pass).execute do |user|
user.skip_reconfirmation!
if user.update_attributes(user_params_with_pass)
end
if result[:status] == :success
format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' }
format.json { head :ok }
else
# restore username to keep form action url.
user.username = params[:id]
format.html { render "edit" }
format.json { render json: user.errors, status: :unprocessable_entity }
format.json { render json: [result[:message]], status: result[:status] }
end
end
end
......@@ -148,13 +152,16 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email
email = user.emails.find(params[:email_id])
email.destroy
user.update_secondary_emails!
success = Emails::DestroyService.new(user, email: email.email).execute
respond_to do |format|
format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") }
format.js { head :ok }
if success
format.html { redirect_back_or_admin_user(notice: 'Successfully removed email.') }
format.json { head :ok }
else
format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') }
format.json { render json: 'There was an error removing the e-mail.', status: 400 }
end
end
end
......@@ -202,4 +209,10 @@ class Admin::UsersController < Admin::ApplicationController
:website_url
]
end
def update_user(&block)
result = Users::UpdateService.new(user).execute(&block)
result[:status] == :success
end
end
......@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
render_404
end
rescue_from(ActionController::UnknownFormat) do
render_404
end
rescue_from Gitlab::Access::AccessDeniedError do |exception|
render_403
end
......
class Profiles::AvatarsController < Profiles::ApplicationController
def destroy
@user = current_user
@user.remove_avatar!
@user.save
Users::UpdateService.new(@user).execute { |user| user.remove_avatar! }
redirect_to profile_path, status: 302
end
......
......@@ -5,9 +5,9 @@ class Profiles::EmailsController < Profiles::ApplicationController
end
def create
@email = current_user.emails.new(email_params)
@email = Emails::CreateService.new(current_user, email_params).execute
if @email.save
if @email.errors.blank?
NotificationService.new.new_email(@email)
else
flash[:alert] = @email.errors.full_messages.first
......@@ -18,9 +18,8 @@ class Profiles::EmailsController < Profiles::ApplicationController
def destroy
@email = current_user.emails.find(params[:id])
@email.destroy
current_user.update_secondary_emails!
Emails::DestroyService.new(current_user, email: @email.email).execute
respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 }
......
......@@ -7,7 +7,9 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def update
if current_user.update_attributes(user_params)
result = Users::UpdateService.new(current_user, user_params).execute
if result[:status] == :success
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
......
......@@ -15,17 +15,17 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return
end
new_password = user_params[:password]
new_password_confirmation = user_params[:password_confirmation]
result = @user.update_attributes(
password: new_password,
password_confirmation: new_password_confirmation,
password_attributes = {
password: user_params[:password],
password_confirmation: user_params[:password_confirmation],
password_automatically_set: false
)
}
result = Users::UpdateService.new(@user, password_attributes).execute
if result[:status] == :success
Users::UpdateService.new(@user, password_expires_at: nil).execute
if result
@user.update_attributes(password_expires_at: nil)
redirect_to root_path, notice: 'Password successfully changed'
else
render :new
......@@ -46,7 +46,9 @@ class Profiles::PasswordsController < Profiles::ApplicationController
return
end
if @user.update_attributes(password_attributes)
result = Users::UpdateService.new(@user, password_attributes).execute
if result[:status] == :success
flash[:notice] = "Password was successfully updated. Please login with it"
redirect_to new_user_session_path
else
......
......@@ -6,7 +6,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def update
begin
if @user.update_attributes(preferences_params)
result = Users::UpdateService.new(user, preferences_params).execute
if result[:status] == :success
flash[:notice] = 'Preferences saved.'
else
flash[:alert] = 'Failed to save preferences.'
......
......@@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.otp_grace_period_started_at = Time.current
end
current_user.save! if current_user.changed?
Users::UpdateService.new(current_user).execute!
if two_factor_authentication_required? && !current_user.two_factor_enabled?
two_factor_authentication_reason(
......@@ -41,9 +41,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def create
if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
Users::UpdateService.new(current_user, otp_required_for_login: true).execute! do |user|
@codes = user.generate_otp_backup_codes!
end
render 'create'
else
......@@ -70,8 +70,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def codes
@codes = current_user.generate_otp_backup_codes!
current_user.save!
Users::UpdateService.new(current_user).execute! do |user|
@codes = user.generate_otp_backup_codes!
end
end
def destroy
......
......@@ -12,39 +12,47 @@ class ProfilesController < Profiles::ApplicationController
user_params.except!(:email) if @user.external_email?
respond_to do |format|
if @user.update_attributes(user_params)
result = Users::UpdateService.new(@user, user_params).execute
if result[:status] == :success
message = "Profile was successfully updated"
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
format.json { render json: { message: message } }
else
message = @user.errors.full_messages.uniq.join('. ')
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
format.json { render json: { message: message }, status: :unprocessable_entity }
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: result[:message] }) }
format.json { render json: result }
end
end
end
def reset_private_token
if current_user.reset_authentication_token!
flash[:notice] = "Private token was successfully reset"
Users::UpdateService.new(@user).execute! do |user|
user.reset_authentication_token!
end
flash[:notice] = "Private token was successfully reset"
redirect_to profile_account_path
end
def reset_incoming_email_token
if current_user.reset_incoming_email_token!
flash[:notice] = "Incoming email token was successfully reset"
Users::UpdateService.new(@user).execute! do |user|
user.reset_incoming_email_token!
end
flash[:notice] = "Incoming email token was successfully reset"
redirect_to profile_account_path
end
def reset_rss_token
if current_user.reset_rss_token!
flash[:notice] = "RSS token was successfully reset"
Users::UpdateService.new(@user).execute! do |user|
user.reset_rss_token!
end
flash[:notice] = "RSS token was successfully reset"
redirect_to profile_account_path
end
......@@ -55,11 +63,12 @@ class ProfilesController < Profiles::ApplicationController
end
def update_username
if @user.update_attributes(username: user_params[:username])
options = { notice: "Username successfully changed" }
result = Users::UpdateService.new(@user, username: user_params[:username]).execute
options = if result[:status] == :success
{ notice: "Username successfully changed" }
else
message = @user.errors.full_messages.uniq.join('. ')
options = { alert: "Username change failed - #{message}" }
{ alert: "Username change failed - #{result[:message]}" }
end
redirect_back_or_default(default: { action: 'show' }, options: options)
......
......@@ -22,6 +22,22 @@ class Projects::DeploymentsController < Projects::ApplicationController
render_404
end
def additional_metrics
return render_404 unless deployment.has_additional_metrics?
respond_to do |format|
format.json do
metrics = deployment.additional_metrics
if metrics.any?
render json: metrics
else
head :no_content
end
end
end
end
private
def deployment
......
......@@ -129,6 +129,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def additional_metrics
respond_to do |format|
format.json do
additional_metrics = environment.additional_metrics || {}
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
end
end
end
private
def verify_api_request!
......
......@@ -575,10 +575,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request_params
params.require(:merge_request)
.permit(merge_request_params_ce)
.permit(merge_request_params_attributes)
end
def merge_request_params_ce
def merge_request_params_attributes
[
:assignee_id,
:description,
......@@ -598,7 +598,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_params
params.permit(:should_remove_source_branch, :commit_message)
params.permit(merge_params_attributes)
end
def merge_params_attributes
[:should_remove_source_branch, :commit_message]
end
# Make sure merge requests created before 8.0
......
......@@ -135,7 +135,12 @@ class Projects::PipelinesController < Projects::ApplicationController
@charts[:week] = Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project)
@charts[:build_times] = Ci::Charts::BuildTime.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
@counts[:success] = @project.pipelines.success.count(:all)
@counts[:failed] = @project.pipelines.failed.count(:all)
end
private
......
class Projects::PrometheusController < Projects::ApplicationController
before_action :authorize_read_project!
before_action :require_prometheus_metrics!
def active_metrics
respond_to do |format|
format.json do
matched_metrics = project.prometheus_service.matched_metrics || {}
if matched_metrics.any?
render json: matched_metrics
else
head :no_content
end
end
end
end
private
def require_prometheus_metrics!
render_404 unless project.prometheus_service.present?
end
end
......@@ -60,10 +60,11 @@ class SessionsController < Devise::SessionsController
return unless user && user.require_password?
token = user.generate_reset_token
user.save
Users::UpdateService.new(user).execute do |user|
@token = user.generate_reset_token
end
redirect_to edit_user_password_path(reset_password_token: token),
redirect_to edit_user_password_path(reset_password_token: @token),
notice: "Please create a password for your new account."
end
......
......@@ -41,7 +41,7 @@ class IssuesFinder < IssuableFinder
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
return Issue.all if user.admin?
return Issue.all if user.full_private_access?
Issue.where('
issues.confidential IS NOT TRUE
......
......@@ -300,4 +300,12 @@ module ApplicationHelper
"https://www.twitter.com/#{name}"
end
end
def can_toggle_new_nav?
Rails.env.development?
end
def show_new_nav?
cookies["new_nav"] == "true"
end
end
......@@ -85,20 +85,20 @@ module CommitsHelper
if @path.blank?
return link_to(
"Browse Files",
_("Browse Files"),
namespace_project_tree_path(project.namespace, project, commit),
class: "btn btn-default"
)
elsif @repo.blob_at(commit.id, @path)
return link_to(
"Browse File",
_("Browse File"),
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "btn btn-default"
)
elsif @path.present?
return link_to(
"Browse Directory",
_("Browse Directory"),
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "btn btn-default"
......
......@@ -17,13 +17,10 @@ module GraphHelper
ids.zip(parent_spaces)
end
def success_ratio(success_builds, failed_builds)
failed_builds = failed_builds.count(:all)
success_builds = success_builds.count(:all)
def success_ratio(counts)
return 100 if counts[:failed].zero?
return 100 if failed_builds.zero?
ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
ratio.to_i
end
end
......@@ -15,7 +15,7 @@ module GroupsHelper
@has_group_title = true
full_title = ''
group.ancestors.each do |parent|
group.ancestors.reverse.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
full_title += '<span class="hidable"> / </span>'.html_safe
end
......
......@@ -4,4 +4,14 @@ module UsersHelper
title: user.email,
class: 'has-tooltip commit-committer-link')
end
def user_email_help_text(user)
return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present?
confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
h('Please click the link in the confirmation email before continuing. It was sent to ') +
content_tag(:strong) { user.unconfirmed_email } + h('.') +
content_tag(:p) { confirmation_link }
end
end
......@@ -11,18 +11,21 @@ module HasStatus
class_methods do
def status_sql
scope = respond_to?(:exclude_ignored) ? exclude_ignored : all
builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
manual = scope.manual.select('count(*)').to_sql
pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql
canceled = scope.canceled.select('count(*)').to_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none
builds = scope_relevant.select('count(*)').to_sql
created = scope_relevant.created.select('count(*)').to_sql
success = scope_relevant.success.select('count(*)').to_sql
manual = scope_relevant.manual.select('count(*)').to_sql
pending = scope_relevant.pending.select('count(*)').to_sql
running = scope_relevant.running.select('count(*)').to_sql
skipped = scope_relevant.skipped.select('count(*)').to_sql
canceled = scope_relevant.canceled.select('count(*)').to_sql
warnings = scope_warnings.select('count(*) > 0').to_sql.presence || 'false'
"(CASE
WHEN (#{builds})=(#{skipped}) AND (#{warnings}) THEN 'success'
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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