Commit 6ebe6792 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into refactor-clusters

parents 3602c0b9 d51ad1ea
7.5 9.0.0
\ No newline at end of file
...@@ -104,8 +104,7 @@ the remaining issues on the GitHub issue tracker. ...@@ -104,8 +104,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute! ## I want to contribute!
If you want to contribute to GitLab, but are not sure where to start, If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
These issues will be of reasonable size and challenge, for anyone to start These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab. contributing to GitLab.
......
...@@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor'; ...@@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select'; import groupsSelect from './groups_select';
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ import NamespaceSelect from './namespace_select';
/* global NewCommitForm */ /* global NewCommitForm */
/* global NewBranchForm */ /* global NewBranchForm */
/* global Project */ /* global Project */
...@@ -575,7 +575,8 @@ import Diff from './diff'; ...@@ -575,7 +575,8 @@ import Diff from './diff';
new UsersSelect(); new UsersSelect();
break; break;
case 'projects': case 'projects':
new NamespaceSelects(); document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
break; break;
case 'labels': case 'labels':
switch (path[2]) { switch (path[2]) {
......
...@@ -30,7 +30,7 @@ const utils = { ...@@ -30,7 +30,7 @@ const utils = {
}, },
isDropDownParts(target) { isDropDownParts(target) {
if (!target || target.tagName === 'HTML') return false; if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN); return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
}, },
}; };
......
...@@ -119,11 +119,9 @@ export default function dropzoneInput(form) { ...@@ -119,11 +119,9 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any) // removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue. // and remove them from dropzone files queue.
$cancelButton.on('click', (e) => { $cancelButton.on('click', (e) => {
const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Dropzone.forElement(target).removeAllFiles(true); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
}); });
// If 'error' event is fired, we store a failed files, // If 'error' event is fired, we store a failed files,
......
...@@ -421,7 +421,11 @@ export default { ...@@ -421,7 +421,11 @@ export default {
</script> </script>
<template> <template>
<div <div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }" class="gl-responsive-table-row"
:class="{
'js-child-row environment-child-row': model.isChildren,
'folder-row': model.isFolder,
}"
role="row"> role="row">
<div class="table-section section-10" role="gridcell"> <div class="table-section section-10" role="gridcell">
<div <div
...@@ -495,15 +499,16 @@ export default { ...@@ -495,15 +499,16 @@ export default {
</a> </a>
</div> </div>
<div class="table-section section-25" role="gridcell"> <div
v-if="!model.isFolder"
class="table-section section-25" role="gridcell">
<div <div
v-if="!model.isFolder"
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Commit Commit
</div> </div>
<div <div
v-if="!model.isFolder && hasLastDeploymentKey" v-if="hasLastDeploymentKey"
class="js-commit-component table-mobile-content"> class="js-commit-component table-mobile-content">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
...@@ -514,21 +519,22 @@ export default { ...@@ -514,21 +519,22 @@ export default {
:author="commitAuthor"/> :author="commitAuthor"/>
</div> </div>
<div <div
v-if="!model.isFolder && !hasLastDeploymentKey" v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content"> class="commit-title table-mobile-content">
No deployments yet No deployments yet
</div> </div>
</div> </div>
<div class="table-section section-10" role="gridcell"> <div
v-if="!model.isFolder"
class="table-section section-10" role="gridcell">
<div <div
v-if="!model.isFolder"
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Updated Updated
</div> </div>
<span <span
v-if="!model.isFolder && canShowDate" v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content"> class="environment-created-date-timeago table-mobile-content">
{{createdDate}} {{createdDate}}
</span> </span>
......
...@@ -4,6 +4,7 @@ import _ from 'underscore'; ...@@ -4,6 +4,7 @@ import _ from 'underscore';
import d3 from 'd3'; import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util'; import ContributorsStatGraphUtil from './stat_graph_contributors_util';
import { n__ } from '../locale';
export default (function() { export default (function() {
function ContributorsStatGraph() {} function ContributorsStatGraph() {}
...@@ -44,7 +45,7 @@ export default (function() { ...@@ -44,7 +45,7 @@ export default (function() {
commits = $('<span/>', { commits = $('<span/>', {
"class": 'graph-author-commits-count' "class": 'graph-author-commits-count'
}); });
commits.text(author.commits + " commits"); commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits); return $('<span/>').append(commits);
}; };
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api'; import Api from './api';
import './lib/utils/url_utility';
(function() { export default class NamespaceSelect {
window.NamespaceSelect = (function() { constructor(opts) {
function NamespaceSelect(opts) { const isFilter = opts.dropdown.dataset.isFilter === 'true';
this.onSelectItem = this.onSelectItem.bind(this); const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
var fieldName, showAny;
this.dropdown = opts.dropdown;
showAny = true;
fieldName = 'namespace_id';
if (this.dropdown.attr('data-field-name')) {
fieldName = this.dropdown.data('fieldName');
}
if (this.dropdown.attr('data-show-any')) {
showAny = this.dropdown.data('showAny');
}
this.dropdown.glDropdown({
filterable: true,
selectable: true,
filterRemote: true,
search: {
fields: ['path']
},
fieldName: fieldName,
toggleLabel: function(selected) {
if (selected.id == null) {
return selected.text;
} else {
return selected.kind + ": " + selected.full_path;
}
},
data: function(term, dataCallback) {
return Api.namespaces(term, function(namespaces) {
var anyNamespace;
if (showAny) {
anyNamespace = {
text: 'Any namespace',
id: null
};
namespaces.unshift(anyNamespace);
namespaces.splice(1, 0, 'divider');
}
return dataCallback(namespaces);
});
},
text: function(namespace) {
if (namespace.id == null) {
return namespace.text;
} else {
return namespace.kind + ": " + namespace.full_path;
}
},
renderRow: this.renderRow,
clicked: this.onSelectItem
});
}
NamespaceSelect.prototype.onSelectItem = function(options) {
const { e } = options;
return e.preventDefault();
};
return NamespaceSelect; $(opts.dropdown).glDropdown({
})(); filterable: true,
selectable: true,
window.NamespaceSelects = (function() { filterRemote: true,
function NamespaceSelects(opts) { search: {
var ref; fields: ['path']
if (opts == null) { },
opts = {}; fieldName: fieldName,
} toggleLabel: function(selected) {
this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); if (selected.id == null) {
this.$dropdowns.each(function(i, dropdown) { return selected.text;
var $dropdown; } else {
$dropdown = $(dropdown); return selected.kind + ": " + selected.full_path;
return new window.NamespaceSelect({ }
dropdown: $dropdown },
data: function(term, dataCallback) {
return Api.namespaces(term, function(namespaces) {
if (isFilter) {
const anyNamespace = {
text: 'Any namespace',
id: null
};
namespaces.unshift(anyNamespace);
namespaces.splice(1, 0, 'divider');
}
return dataCallback(namespaces);
}); });
}); },
} text: function(namespace) {
if (namespace.id == null) {
return NamespaceSelects; return namespace.text;
})(); } else {
}).call(window); return namespace.kind + ": " + namespace.full_path;
}
},
renderRow: this.renderRow,
clicked(options) {
if (!isFilter) {
const { e } = options;
e.preventDefault();
}
},
url(namespace) {
return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
},
});
}
}
...@@ -122,7 +122,9 @@ ...@@ -122,7 +122,9 @@
// we need to do this to prevent noteForm inconsistent content warning // we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content // this is something we intentionally do so we need to recover the content
this.note.note = noteText; this.note.note = noteText;
this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better if (this.$refs.noteBody) {
this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
}
}, },
}, },
created() { created() {
......
<script> <script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
/** /**
* Renders either a cancel, retry or play icon pointing to the given path. * Renders either a cancel, retry or play icon pointing to the given path.
...@@ -29,17 +29,18 @@ ...@@ -29,17 +29,18 @@
}, },
}, },
components: {
icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
computed: { computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
cssClass() { cssClass() {
return `js-${gl.text.dasherize(this.actionIcon)}`; const actionIconDash = gl.text.dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
}, },
}; };
...@@ -50,14 +51,9 @@ ...@@ -50,14 +51,9 @@
:data-method="actionMethod" :data-method="actionMethod"
:title="tooltipText" :title="tooltipText"
:href="link" :href="link"
class="ci-action-icon-container" class="ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"> data-container="body">
<icon :name="actionIcon"/>
<i
class="ci-action-icon-wrapper"
:class="cssClass"
v-html="actionIconSvg"
aria-hidden="true"
/>
</a> </a>
</template> </template>
<script> <script>
import getActionIcon from '../../../vue_shared/ci_action_icons'; import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
...@@ -29,14 +29,12 @@ ...@@ -29,14 +29,12 @@
}, },
}, },
directives: { components: {
tooltip, icon,
}, },
computed: { directives: {
actionIconSvg() { tooltip,
return getActionIcon(this.actionIcon);
},
}, },
}; };
</script> </script>
...@@ -49,7 +47,7 @@ ...@@ -49,7 +47,7 @@
rel="nofollow" rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon" class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body" data-container="body"
v-html="actionIconSvg"
aria-label="Job's action"> aria-label="Job's action">
<icon :name="actionIcon"/>
</a> </a>
</template> </template>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "icon_action_retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "icon_action_retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
*/ */
import Flash from '../../flash'; import Flash from '../../flash';
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -45,6 +45,7 @@ export default { ...@@ -45,6 +45,7 @@ export default {
components: { components: {
loadingIcon, loadingIcon,
icon,
}, },
updated() { updated() {
...@@ -122,8 +123,8 @@ export default { ...@@ -122,8 +123,8 @@ export default {
return `ci-status-icon-${this.stage.status.group}`; return `ci-status-icon-${this.stage.status.group}`;
}, },
svgIcon() { borderlessIcon() {
return borderlessStatusIconEntityMap[this.stage.status.icon]; return `${this.stage.status.icon}_borderless`;
}, },
}, },
}; };
...@@ -145,9 +146,10 @@ export default { ...@@ -145,9 +146,10 @@ export default {
aria-expanded="false"> aria-expanded="false">
<span <span
v-html="svgIcon"
aria-hidden="true" aria-hidden="true"
:aria-label="stage.title"> :aria-label="stage.title">
<icon
:name="borderlessIcon"/>
</span> </span>
<i <i
......
...@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen" @toggle="toggleOpen"
@submit="onSubmit"> @submit="onSubmit">
<template slot="body" scope="props"> <template slot="body" slot-scope="props">
<p v-html="props.text"></p> <p v-html="props.text"></p>
<form <form
......
...@@ -27,6 +27,8 @@ export default { ...@@ -27,6 +27,8 @@ export default {
'changeFileContent', 'changeFileContent',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return;
if (this.monacoInstance) { if (this.monacoInstance) {
this.monacoInstance.setModel(null); this.monacoInstance.setModel(null);
} }
...@@ -94,8 +96,12 @@ export default { ...@@ -94,8 +96,12 @@ export default {
<template> <template>
<div <div
id="ide" id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
</div> </div>
</template> </template>
function expandSectionParent($section, $content) {
$section.addClass('expanded');
$content.off('animationend.expandSectionParent');
}
function expandSection($section) { function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse'); $section.find('.js-settings-toggle').text('Collapse');
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
const $content = $section.find('.settings-content'); $section.addClass('expanded');
$content.addClass('expanded').off('scroll.expandSection').scrollTop(0); if (!$section.hasClass('no-animate')) {
$section.addClass('animating')
if ($content.hasClass('no-animate')) { .one('animationend.animateSection', () => $section.removeClass('animating'));
expandSectionParent($section, $content);
} else {
$content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
} }
} }
function closeSection($section) { function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand'); $section.find('.js-settings-toggle').text('Expand');
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
const $content = $section.find('.settings-content');
$content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded'); $section.removeClass('expanded');
if (!$section.hasClass('no-animate')) {
$section.addClass('animating')
.one('animationend.animateSection', () => $section.removeClass('animating'));
}
} }
function toggleSection($section) { function toggleSection($section) {
const $content = $section.find('.settings-content'); $section.removeClass('no-animate');
$content.removeClass('no-animate'); if ($section.hasClass('expanded')) {
if ($content.hasClass('expanded')) {
closeSection($section); closeSection($section);
} else { } else {
expandSection($section); expandSection($section);
...@@ -39,10 +31,19 @@ export default function initSettingsPanels() { ...@@ -39,10 +31,19 @@ export default function initSettingsPanels() {
$('.settings').each((i, elm) => { $('.settings').each((i, elm) => {
const $section = $(elm); const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
if (!$section.hasClass('expanded')) {
$section.find('.settings-content').on('scroll.expandSection', () => {
$section.removeClass('no-animate');
expandSection($section);
});
}
}); });
if (location.hash) { if (location.hash) {
expandSection($(location.hash)); const $target = $(location.hash);
if ($target.length && $target.hasClass('.settings')) {
expandSection($target);
}
} }
} }
import PipelineStage from '../../pipelines/components/stage.vue'; import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
components: { components: {
'pipeline-stage': PipelineStage, 'pipeline-stage': PipelineStage,
ciIcon, ciIcon,
icon,
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -20,9 +21,6 @@ export default { ...@@ -20,9 +21,6 @@ export default {
return hasCI && !ciStatus; return hasCI && !ciStatus;
}, },
svg() {
return statusIconEntityMap.icon_status_failed;
},
stageText() { stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
}, },
...@@ -38,8 +36,10 @@ export default { ...@@ -38,8 +36,10 @@ export default {
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
<span <span
v-html="svg" aria-hidden="true">
aria-hidden="true"></span> <icon
name="status_failed"/>
</span>
</div> </div>
<div class="media-body"> <div class="media-body">
Could not connect to the CI server. Please check your settings and try again Could not connect to the CI server. Please check your settings and try again
......
import cancelSVG from 'icons/_icon_action_cancel.svg';
import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg';
import stopSVG from 'icons/_icon_action_stop.svg';
/**
* For the provided action returns the respective SVG
*
* @param {String} action
* @return {SVG|String}
*/
export default function getActionIcon(action) {
const icons = {
icon_action_cancel: cancelSVG,
icon_action_play: playSVG,
icon_action_retry: retrySVG,
icon_action_stop: stopSVG,
};
return icons[action] || '';
}
import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
import CREATED_SVG from 'icons/_icon_status_created.svg';
import FAILED_SVG from 'icons/_icon_status_failed.svg';
import MANUAL_SVG from 'icons/_icon_status_manual.svg';
import PENDING_SVG from 'icons/_icon_status_pending.svg';
import RUNNING_SVG from 'icons/_icon_status_running.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
import SUCCESS_SVG from 'icons/_icon_status_success.svg';
import WARNING_SVG from 'icons/_icon_status_warning.svg';
export const borderlessStatusIconEntityMap = {
icon_status_canceled: BORDERLESS_CANCELED_SVG,
icon_status_created: BORDERLESS_CREATED_SVG,
icon_status_failed: BORDERLESS_FAILED_SVG,
icon_status_manual: BORDERLESS_MANUAL_SVG,
icon_status_pending: BORDERLESS_PENDING_SVG,
icon_status_running: BORDERLESS_RUNNING_SVG,
icon_status_skipped: BORDERLESS_SKIPPED_SVG,
icon_status_success: BORDERLESS_SUCCESS_SVG,
icon_status_warning: BORDERLESS_WARNING_SVG,
};
export const statusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
computed: { computed: {
cssClass() { cssClass() {
const className = this.status.group; const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
......
<script> <script>
import { statusIconEntityMap } from '../ci_status_icons'; import icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
}, },
}, },
computed: { components: {
statusIconSvg() { icon,
return statusIconEntityMap[this.status.icon]; },
},
computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
...@@ -44,7 +44,8 @@ ...@@ -44,7 +44,8 @@
</script> </script>
<template> <template>
<span <span
:class="cssClass" :class="cssClass">
v-html="statusIconSvg"> <icon
:name="status.icon"/>
</span> </span>
</template> </template>
<script>
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
<icon
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
export default {
props: {
name: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 0,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script>
<template>
<svg
:class="[iconSizeClass, cssClasses]">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
</template>
...@@ -56,4 +56,4 @@ ...@@ -56,4 +56,4 @@
@import "framework/icons"; @import "framework/icons";
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive-tables"; @import "framework/responsive_tables";
...@@ -5,32 +5,6 @@ ...@@ -5,32 +5,6 @@
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.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; }
.prepend-left-20 { margin-left: 20px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; } .light { color: $common-gray; }
...@@ -448,3 +422,30 @@ table { ...@@ -448,3 +422,30 @@ table {
pointer-events: none; pointer-events: none;
opacity: .5; opacity: .5;
} }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.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; }
.prepend-left-20 { margin-left: 20px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
...@@ -3,57 +3,74 @@ ...@@ -3,57 +3,74 @@
max-width: #{$max + '%'}; max-width: #{$max + '%'};
} }
.gl-responsive-table-row-layout {
width: 100%;
@media (min-width: $screen-md-min) {
display: flex;
align-items: center;
& > &:not(:first-child) {
margin-top: $gl-padding;
}
}
}
.gl-responsive-table-row { .gl-responsive-table-row {
@extend .gl-responsive-table-row-layout;
margin-top: 10px; margin-top: 10px;
border: 1px solid $border-color; border: 1px solid $border-color;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding: 15px 0;
margin: 0; margin: 0;
display: flex; padding: $gl-padding 0;
align-items: center;
border: none; border: none;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
} }
}
.gl-responsive-table-row-col-span {
flex-wrap: wrap;
}
.table-section { .table-section {
white-space: nowrap; white-space: nowrap;
$section-widths: 10 15 20 25 30 40; $section-widths: 10 15 20 25 30 40 100;
@each $width in $section-widths { @each $width in $section-widths {
&.section-#{$width} { &.section-#{$width} {
flex: 0 0 #{$width + '%'}; flex: 0 0 #{$width + '%'};
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
max-width: #{$width + '%'}; max-width: #{$width + '%'};
}
} }
} }
}
&:not(.table-button-footer) { @media (max-width: $screen-sm-max) {
@media (max-width: $screen-sm-max) { display: flex;
display: flex; align-self: stretch;
align-self: stretch; padding: 10px;
padding: 10px; align-items: center;
align-items: center; min-height: 62px;
min-height: 62px;
&:not(:first-of-type) { &:not(:first-child) {
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
}
}
} }
}
&.section-wrap { &.section-wrap {
white-space: normal; white-space: normal;
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
flex-wrap: wrap; flex-wrap: wrap;
}
} }
} }
}
&.section-align-top {
align-self: flex-start;
}
}
.table-button-footer { .table-button-footer {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
...@@ -61,12 +78,13 @@ ...@@ -61,12 +78,13 @@
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
background-color: $gray-normal; display: block;
align-self: stretch; align-self: stretch;
min-height: 0;
background-color: $gray-normal;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
.table-action-buttons { .table-action-buttons {
padding: 10px 5px;
display: flex; display: flex;
.btn { .btn {
...@@ -77,7 +95,14 @@ ...@@ -77,7 +95,14 @@
> .external-url, > .external-url,
> .btn { > .btn {
flex: 1 1 28px; flex: 1 1 28px;
margin: 0 5px;
&:not(:first-child) {
margin-left: 5px;
}
&:not(:last-child) {
margin-right: 5px;
}
} }
.dropdown-new { .dropdown-new {
......
...@@ -333,8 +333,10 @@ ...@@ -333,8 +333,10 @@
svg { svg {
position: relative; position: relative;
top: 2px; top: 3px;
margin-right: 3px; margin-right: 3px;
width: 14px;
height: 14px;
} }
} }
...@@ -348,9 +350,10 @@ ...@@ -348,9 +350,10 @@
svg { svg {
position: relative; position: relative;
top: 2px; top: 3px;
margin-right: 3px; margin-right: 3px;
height: 13px; height: 14px;
width: 14px;
} }
a { a {
...@@ -369,7 +372,7 @@ ...@@ -369,7 +372,7 @@
.build-job { .build-job {
position: relative; position: relative;
.fa-arrow-right { .icon-arrow-right {
position: absolute; position: absolute;
left: 15px; left: 15px;
top: 20px; top: 20px;
...@@ -379,7 +382,7 @@ ...@@ -379,7 +382,7 @@
&.active { &.active {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
.fa-arrow-right { .icon-arrow-right {
display: block; display: block;
} }
} }
...@@ -392,8 +395,7 @@ ...@@ -392,8 +395,7 @@
background-color: $row-hover; background-color: $row-hover;
} }
.fa-refresh { .icon-retry {
font-size: 13px;
margin-left: 3px; margin-left: 3px;
} }
} }
......
...@@ -2,8 +2,4 @@ ...@@ -2,8 +2,4 @@
.clipboard-addon { .clipboard-addon {
background-color: $white-light; background-color: $white-light;
} }
.alert-block {
margin-bottom: 10px;
}
} }
...@@ -133,12 +133,11 @@ ...@@ -133,12 +133,11 @@
} }
.folder-row { .folder-row {
padding: 15px 0; border-left: none;
border-bottom: 1px solid $white-normal; border-right: none;
@media (max-width: $screen-sm-max) { @media (min-width: $screen-sm-max) {
border-top: 1px solid $white-normal; border-top: none;
margin-top: 10px;
} }
} }
......
...@@ -165,8 +165,9 @@ ...@@ -165,8 +165,9 @@
z-index: 300; z-index: 300;
} }
.ci-action-icon-wrapper { .ci-action-icon-wrapper svg {
line-height: 16px; width: 16px;
height: 16px;
} }
} }
......
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
} }
.pipeline-actions { .pipeline-actions {
padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines. min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default { .btn-default {
...@@ -452,7 +451,7 @@ ...@@ -452,7 +451,7 @@
} }
// Action Icons in big pipeline-graph nodes // Action Icons in big pipeline-graph nodes
.ci-action-icon-container .ci-action-icon-wrapper { .ci-action-icon-container.ci-action-icon-wrapper {
height: 30px; height: 30px;
width: 30px; width: 30px;
background: $white-light; background: $white-light;
...@@ -468,8 +467,18 @@ ...@@ -468,8 +467,18 @@
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
position: relative; position: relative;
left: -1px; left: 5px;
top: -1px; top: 2px;
width: 18px;
height: 18px;
}
&.play {
svg {
width: #{$ci-action-icon-size - 8};
height: #{$ci-action-icon-size - 8};
left: 8px;
}
} }
&:hover svg { &:hover svg {
...@@ -721,17 +730,49 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -721,17 +730,49 @@ button.mini-pipeline-graph-dropdown-toggle {
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
width: $ci-action-icon-size; width: #{$ci-action-icon-size - 6};
height: $ci-action-icon-size; height: #{$ci-action-icon-size - 6};
left: -6px; left: -3px;
position: relative; position: relative;
top: -3px; top: -2px;
} }
&:hover svg, &:hover svg,
&:focus svg { &:focus svg {
fill: $gl-text-color; fill: $gl-text-color;
} }
&.icon-action-retry,
&.icon-action-play {
svg {
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: 8px;
}
}
svg.icon-action-stop,
svg.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
}
svg.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
}
svg.icon-action-retry {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
} }
// link to the build // link to the build
......
...@@ -23,15 +23,14 @@ ...@@ -23,15 +23,14 @@
} }
.settings { .settings {
overflow: hidden;
border-bottom: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker;
&:first-of-type { &:first-of-type {
margin-top: 10px; margin-top: 10px;
} }
&.expanded { &.animating {
overflow: visible; overflow: hidden;
} }
} }
...@@ -56,14 +55,18 @@ ...@@ -56,14 +55,18 @@
overflow-y: scroll; overflow-y: scroll;
padding-right: 110px; padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out; animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
pointer-events: none;
&.expanded { .settings.expanded & {
max-height: none; max-height: none;
overflow-y: visible; overflow-y: visible;
animation: expandMaxHeight 300ms ease-in; animation: expandMaxHeight 300ms ease-in;
// Reset and allow clicks again when expanded
pointer-events: auto;
} }
&.no-animate { .settings.no-animate & {
animation: none; animation: none;
} }
......
...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController ...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build @impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute @inactive_impersonation_tokens = finder(state: 'inactive').execute
......
...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base ...@@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include WithPerformanceBar include WithPerformanceBar
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_personal_access_token!
before_action :authenticate_user_from_rss_token! before_action :authenticate_user_from_rss_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base ...@@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base
return try(:authenticated_user) return try(:authenticated_user)
end end
# This filter handles both private tokens and personal access tokens def authenticate_user_from_personal_access_token!
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present? return unless token.present?
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) user = User.find_by_personal_access_token(token)
sessionless_sign_in(user) sessionless_sign_in(user)
end end
......
...@@ -7,6 +7,54 @@ module IssuableActions ...@@ -7,6 +7,54 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end end
def show
respond_to do |format|
format.html do
render show_view
end
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
end
end
def update
@issuable = update_service.execute(issuable)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_entity_json
end
end
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
response = {
title: view_context.markdown_field(issuable, :title),
title_text: issuable.title,
description: view_context.markdown_field(issuable, :description),
description_text: issuable.description,
task_status: issuable.task_status
}
if issuable.edited?
response[:updated_at] = issuable.updated_at
response[:updated_by_name] = issuable.last_edited_by.name
response[:updated_by_path] = user_path(issuable.last_edited_by)
end
render json: response
end
def destroy def destroy
issuable.destroy issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
...@@ -68,6 +116,10 @@ module IssuableActions ...@@ -68,6 +116,10 @@ module IssuableActions
end end
end end
def authorize_update_issuable!
render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
end
def bulk_update_params def bulk_update_params
permitted_keys = [ permitted_keys = [
:issuable_ids, :issuable_ids,
...@@ -92,4 +144,24 @@ module IssuableActions ...@@ -92,4 +144,24 @@ module IssuableActions
def resource_name def resource_name
@resource_name ||= controller_name.singularize @resource_name ||= controller_name.singularize
end end
def render_entity_json
if @issuable.valid?
render json: serializer.represent(@issuable)
else
render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
end
end
def show_view
'show'
end
def serializer
raise NotImplementedError
end
def update_service
raise NotImplementedError
end
end end
...@@ -30,11 +30,11 @@ class JwtController < ApplicationController ...@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized render_unauthorized
end end
end end
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def render_missing_personal_token def render_missing_personal_access_token
render json: { render json: {
errors: [ errors: [
{ code: 'UNAUTHORIZED', { code: 'UNAUTHORIZED',
......
...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
......
...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end end
end end
def reset_private_token
Users::UpdateService.new(current_user, user: @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 def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token! user.reset_incoming_email_token!
...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset" flash[:notice] = "Incoming email token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def reset_rss_token def reset_rss_token
...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset" flash[:notice] = "RSS token was successfully reset"
redirect_to profile_account_path redirect_to profile_personal_access_tokens_path
end end
def audit_log def audit_log
......
...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401 render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_token render_missing_personal_access_token
end end
def basic_auth_provided? def basic_auth_provided?
...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end end
def render_missing_personal_token def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \ render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \ "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}", "You can generate one at #{profile_personal_access_tokens_url}",
......
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update, :move] before_action :authorize_update_issuable!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def show
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
respond_to do |format|
format.html
format.json do
render json: serializer.represent(@issue, serializer: params[:serializer])
end
end
end
def discussions def discussions
notes = @issue.notes notes = @issue.notes
.inc_relations_for_view .inc_relations_for_view
...@@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def update
update_params = issue_params.merge(spammable_params)
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_issue_json
end
end
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
def move def move
params.require(:move_to_project_id) params.require(:move_to_project_id)
...@@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
response = {
title: view_context.markdown_field(@issue, :title),
title_text: @issue.title,
description: view_context.markdown_field(@issue, :description),
description_text: @issue.description,
task_status: @issue.task_status
}
if @issue.edited?
response[:updated_at] = @issue.updated_at
response[:updated_by_name] = @issue.last_edited_by.name
response[:updated_by_path] = user_path(@issue.last_edited_by)
end
render json: response
end
def create_merge_request def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
...@@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue) project_issue_path(@project, @issue)
end end
def authorize_update_issue!
render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
...@@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer def serializer
IssueSerializer.new(current_user: current_user, project: issue.project) IssueSerializer.new(current_user: current_user, project: issue.project)
end end
def update_service
update_params = issue_params.merge(spammable_params)
Issues::UpdateService.new(project, current_user, update_params)
end
end end
...@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
...@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request alias_method :awardable, :merge_request
def authorize_update_merge_request!
return render_404 unless can?(current_user, :update_merge_request, @merge_request)
end
def authorize_admin_merge_request!
return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
end
def validates_merge_request def validates_merge_request
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
......
...@@ -63,34 +63,34 @@ module CiStatusHelper ...@@ -63,34 +63,34 @@ module CiStatusHelper
def ci_icon_for_status(status) def ci_icon_for_status(status)
if detailed_status?(status) if detailed_status?(status)
return custom_icon(status.icon) return sprite_icon(status.icon)
end end
icon_name = icon_name =
case status case status
when 'success' when 'success'
'icon_status_success' 'status_success'
when 'success_with_warnings' when 'success_with_warnings'
'icon_status_warning' 'status_warning'
when 'failed' when 'failed'
'icon_status_failed' 'status_failed'
when 'pending' when 'pending'
'icon_status_pending' 'status_pending'
when 'running' when 'running'
'icon_status_running' 'status_running'
when 'play' when 'play'
'icon_play' 'play'
when 'created' when 'created'
'icon_status_created' 'status_created'
when 'skipped' when 'skipped'
'icon_status_skipped' 'status_skipped'
when 'manual' when 'manual'
'icon_status_manual' 'status_manual'
else else
'icon_status_canceled' 'status_canceled'
end end
custom_icon(icon_name) sprite_icon(icon_name, size: 16)
end end
def pipeline_status_cache_key(pipeline_status) def pipeline_status_cache_key(pipeline_status)
......
...@@ -71,11 +71,13 @@ module GitlabRoutingHelper ...@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args) project_commit_url(entity.project, entity.sha, *args)
end end
def preview_markdown_path(project, *args) def preview_markdown_path(parent, *args)
return group_preview_markdown_path(parent) if parent.is_a?(Group)
if @snippet.is_a?(PersonalSnippet) if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path preview_markdown_snippets_path
else else
preview_markdown_project_path(project, *args) preview_markdown_project_path(parent, *args)
end end
end end
......
...@@ -211,15 +211,13 @@ module IssuablesHelper ...@@ -211,15 +211,13 @@ module IssuablesHelper
def issuable_initial_data(issuable) def issuable_initial_data(issuable)
data = { data = {
endpoint: project_issue_path(@project, issuable), endpoint: issuable_path(issuable),
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
...@@ -227,6 +225,12 @@ module IssuablesHelper ...@@ -227,6 +225,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status initialTaskStatus: issuable.task_status
} }
if parent.is_a?(Group)
data[:groupPath] = parent.path
else
data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
end
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
data.to_json data.to_json
...@@ -263,12 +267,7 @@ module IssuablesHelper ...@@ -263,12 +267,7 @@ module IssuablesHelper
end end
def issuable_path(issuable, *options) def issuable_path(issuable, *options)
case issuable polymorphic_path(issuable, *options)
when Issue
issue_path(issuable, *options)
when MergeRequest
merge_request_path(issuable, *options)
end
end end
def issuable_url(issuable, *options) def issuable_url(issuable, *options)
...@@ -369,4 +368,8 @@ module IssuablesHelper ...@@ -369,4 +368,8 @@ module IssuablesHelper
fullPath: @project.full_path fullPath: @project.full_path
} }
end end
def parent
@project || @group
end
end end
...@@ -49,7 +49,8 @@ module CacheMarkdownField ...@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains # Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project) project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project) group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
......
...@@ -14,7 +14,6 @@ module Issuable ...@@ -14,7 +14,6 @@ module Issuable
include StripAttribute include StripAttribute
include Awardable include Awardable
include Taskable include Taskable
include TimeTrackable
include Importable include Importable
include Editable include Editable
include AfterCommitQueue include AfterCommitQueue
...@@ -95,8 +94,6 @@ module Issuable ...@@ -95,8 +94,6 @@ module Issuable
strip_attributes :title strip_attributes :title
acts_as_paranoid
after_save :record_metrics, unless: :imported? after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
......
# Placeholder class for model that is implemented in EE
# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base
# TODO: this will be implemented as part of #3853
def to_reference
end
end
...@@ -180,6 +180,12 @@ class Group < Namespace ...@@ -180,6 +180,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user) add_user(user, :owner, current_user: current_user)
end end
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
max_member_access_for_user(user) >= min_access_level
end
def has_owner?(user) def has_owner?(user)
return false unless user return false unless user
......
...@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base ...@@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys include FasterCacheKeys
include RelativePositioning include RelativePositioning
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
...@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base ...@@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base
end end
end end
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
......
...@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base
include Sortable include Sortable
include IgnorableColumn include IgnorableColumn
include CreatedAtFilterable include CreatedAtFilterable
include TimeTrackable
ignore_column :locked_at ignore_column :locked_at
...@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit after_save :keep_around_commit
acts_as_paranoid
def self.reference_prefix def self.reference_prefix
'!' '!'
end end
......
...@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect information about commits and diff from repository # Collect information about commits and diff from repository
# and save it to the database as serialized data # and save it to the database as serialized data
def save_git_content def save_git_content
MergeRequest
.where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
.update_all(latest_merge_request_diff_id: self.id)
ensure_commit_shas ensure_commit_shas
save_commits save_commits
save_diffs save_diffs
......
...@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base ...@@ -69,7 +69,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true validates :note, presence: true
validates :project, presence: true, unless: :for_personal_snippet? validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base ...@@ -114,7 +114,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create before_validation :set_discussion_id, on: :create
after_save :keep_around_commit, unless: :for_personal_snippet? after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache after_save :expire_etag_cache
after_destroy :expire_etag_cache after_destroy :expire_etag_cache
...@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base ...@@ -208,6 +208,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet) noteable.is_a?(PersonalSnippet)
end end
def for_project_noteable?
!for_personal_snippet?
end
def skip_project_check? def skip_project_check?
for_personal_snippet? for_personal_snippet?
end end
......
...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken ...@@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User' belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner alias_attribute :user, :resource_owner
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
else
super
end
end
end end
...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base ...@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email ignore_column :external_email
ignore_column :email_provider ignore_column :email_provider
ignore_column :authentication_token
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token add_authentication_token_field :rss_token
...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base ...@@ -163,7 +163,7 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed? before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed? before_validation :set_public_email, if: :public_email_changed?
before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base ...@@ -185,8 +185,6 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files] enum project_view: [:readme, :activity, :files]
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do state_machine :state, initial: :active do
......
class IssuableEntity < Grape::Entity class IssuableEntity < Grape::Entity
include RequestAwareEntity
expose :id expose :id
expose :iid expose :iid
expose :author_id expose :author_id
expose :description expose :description
expose :lock_version expose :lock_version
expose :milestone_id expose :milestone_id
expose :state
expose :title expose :title
expose :updated_by_id expose :updated_by_id
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :deleted_at
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
end end
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :branch_name expose :branch_name
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
......
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
include RequestAwareEntity include TimeTrackableEntity
expose :state
expose :deleted_at
expose :in_progress_merge_commit_sha expose :in_progress_merge_commit_sha
expose :merge_commit_sha expose :merge_commit_sha
expose :merge_error expose :merge_error
......
module TimeTrackableEntity
extend ActiveSupport::Concern
extend Grape
included do
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end
end
...@@ -39,11 +39,8 @@ class AccessTokenValidationService ...@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym) token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope| required_scopes.any? do |scope|
if scope.respond_to?(:sufficient?) scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
scope.sufficient?(token_scopes, request) scope.sufficient?(token_scopes, request)
else
API::Scope.new(scope).sufficient?(token_scopes, request)
end
end end
end end
end end
......
module Issuable
class CommonSystemNotesService < ::BaseService
attr_reader :issuable
def execute(issuable, old_labels)
@issuable = issuable
if issuable.previous_changes.include?('title')
create_title_change_note(issuable.previous_changes['title'].first)
end
handle_description_change_note
handle_time_tracking_note if issuable.is_a?(TimeTrackable)
create_labels_note(old_labels) if issuable.labels != old_labels
create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
create_milestone_note if issuable.previous_changes.include?('milestone_id')
end
private
def handle_time_tracking_note
if issuable.previous_changes.include?('time_estimate')
create_time_estimate_note
end
if issuable.time_spent?
create_time_spent_note
end
end
def handle_description_change_note
if issuable.previous_changes.include?('description')
if issuable.tasks? && issuable.updated_tasks.any?
create_task_status_note
else
# TODO: Show this note if non-task content was modified.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
create_description_change_note
end
end
end
def create_labels_note(old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
end
def create_title_change_note(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
end
def create_description_change_note
SystemNoteService.change_description(issuable, issuable.project, current_user)
end
def create_task_status_note
issuable.updated_tasks.each do |task|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
end
end
def create_time_estimate_note
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
end
def create_time_spent_note
SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
end
def create_milestone_note
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
def create_discussion_lock_note
SystemNoteService.discussion_lock(issuable, current_user)
end
end
end
class IssuableBaseService < BaseService class IssuableBaseService < BaseService
private private
def create_milestone_note(issuable)
SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone)
end
def create_labels_note(issuable, old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
def create_title_change_note(issuable, old_title)
SystemNoteService.change_title(
issuable, issuable.project, current_user, old_title)
end
def create_description_change_note(issuable)
SystemNoteService.change_description(issuable, issuable.project, current_user)
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
old_branch, new_branch)
end
def create_task_status_note(issuable)
issuable.updated_tasks.each do |task|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
end
end
def create_time_estimate_note(issuable)
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
end
def create_time_spent_note(issuable)
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
end
def create_discussion_lock_note(issuable)
SystemNoteService.discussion_lock(issuable, current_user)
end
def filter_params(issuable) def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}" ability_name = :"admin_#{issuable.to_ability_name}"
unless can?(current_user, ability_name, project) unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:labels) params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)
...@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService ...@@ -233,15 +187,14 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets # We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save. # the changed fields upon calling #save.
update_project_counters = issuable.update_project_counter_caches? update_project_counters = issuable.project && issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save } if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field # We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels) Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels)
end end
change_discussion_lock(issuable)
handle_changes( handle_changes(
issuable, issuable,
old_labels: old_labels, old_labels: old_labels,
...@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService ...@@ -300,12 +253,6 @@ class IssuableBaseService < BaseService
end end
end end
def change_discussion_lock(issuable)
if issuable.previous_changes.include?('discussion_locked')
create_discussion_lock_note(issuable)
end
end
def toggle_award(issuable) def toggle_award(issuable)
award = params.delete(:emoji_award) award = params.delete(:emoji_award)
if award if award
...@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService ...@@ -328,35 +275,17 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed attrs_changed || labels_changed || assignees_changed
end end
def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
end
if issuable.previous_changes.include?('description')
if issuable.tasks? && issuable.updated_tasks.any?
create_task_status_note(issuable)
else
# TODO: Show this note if non-task content was modified.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
create_description_change_note(issuable)
end
end
if issuable.previous_changes.include?('time_estimate')
create_time_estimate_note(issuable)
end
if issuable.time_spent?
create_time_spent_note(issuable)
end
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
def invalidate_cache_counts(issuable, users: []) def invalidate_cache_counts(issuable, users: [])
users.each do |user| users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end end
end end
# override if needed
def handle_changes(issuable, options)
end
# override if needed
def execute_hooks(issuable, action = 'open', params = {})
end
end end
...@@ -27,10 +27,6 @@ module Issues ...@@ -27,10 +27,6 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users) todo_service.update_issue(issue, current_user, old_mentioned_users)
end end
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees)
......
...@@ -40,10 +40,6 @@ module MergeRequests ...@@ -40,10 +40,6 @@ module MergeRequests
merge_request.target_branch) merge_request.target_branch)
end end
if merge_request.previous_changes.include?('milestone_id')
create_milestone_note(merge_request)
end
if merge_request.previous_changes.include?('assignee_id') if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request) create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user) notification_service.reassigned_merge_request(merge_request, current_user)
...@@ -111,5 +107,11 @@ module MergeRequests ...@@ -111,5 +107,11 @@ module MergeRequests
end end
end end
end end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
old_branch, new_branch)
end
end end
end end
...@@ -6,8 +6,7 @@ class MetricsService ...@@ -6,8 +6,7 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck, Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck, Gitlab::HealthChecks::Redis::SharedStateCheck
Gitlab::HealthChecks::FsShardsCheck
].freeze ].freeze
def prometheus_metrics_text def prometheus_metrics_text
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td %td
= truncate(hook_log.url, length: 50) = truncate(hook_log.url, length: 50)
%td.light %td.light
#{number_with_precision(hook_log.execution_duration, precision: 2)} ms #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light %td.light
= time_ago_with_tooltip(hook_log.created_at) = time_ago_with_tooltip(hook_log.created_at)
%td %td
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= hidden_field_tag :namespace_id, params[:namespace_id] = hidden_field_tag :namespace_id, params[:namespace_id]
- namespace = Namespace.find(params[:namespace_id]) - namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}" - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right .dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces') = dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace") = dropdown_filter("Search for Namespace")
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
= f.label :new_namespace_id, "Namespace", class: 'control-label' = f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10 .col-sm-10
.dropdown .dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_title('Namespaces') = dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace") = dropdown_filter("Search for Namespace")
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
- if link && status.has_details? - if link && status.has_details?
= link_to status.details_path, class: css_classes, title: title do = link_to status.details_path, class: css_classes, title: title do
= custom_icon(status.icon) = sprite_icon(status.icon)
= status.text = status.text
- else - else
%span{ class: css_classes, title: title } %span{ class: css_classes, title: title }
= custom_icon(status.icon) = sprite_icon(status.icon)
= status.text = status.text
...@@ -7,13 +7,13 @@ ...@@ -7,13 +7,13 @@
- if status.has_details? - if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= custom_icon(status.icon) %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name %span.ci-build-text= subject.name
- else - else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= custom_icon(status.icon) %span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name %span.ci-build-text= subject.name
- if status.has_action? - if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= custom_icon(status.action_icon) = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
To do Todos
%span.badge %span.badge
= number_with_delimiter(todos_pending_count) = number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }> %li.todos-done{ class: active_when(params[:state] == 'done') }>
......
- name = label.parameterize
- attribute = name.underscore
.reset-action
%p.cgray
= label_tag name, label, class: "label-light"
= text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
= help_text
.prepend-top-default
= link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
...@@ -6,22 +6,6 @@ ...@@ -6,22 +6,6 @@
.alert.alert-info .alert.alert-info
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Private Tokens
%p
Keep these tokens secret, anyone with access to them can interact with
GitLab as if they were you.
.col-lg-8.private-tokens-reset
= render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
= render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
- if incoming_email_token_enabled?
= render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
%hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -30,3 +30,40 @@ ...@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
RSS token
%p
Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
%p
It cannot be used to access any other data.
.col-lg-8.rss-token-reset
= label_tag :rss_token, 'RSS token', class: "label-light"
= text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
You should
= link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
if that ever happens.
- if incoming_email_token_enabled?
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Incoming email token
%p
Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
%p
It cannot be used to access any other data.
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.help-block
Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
You should
= link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
if that ever happens.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project) - project = local_assigns.fetch(:project)
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Export project Export project
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.bs-callout.bs-callout-info .bs-callout.bs-callout-info
%p.append-bottom-0 %p.append-bottom-0
%p %p
......
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Deploy Keys Deploy Keys
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
%h5.prepend-top-0 %h5.prepend-top-0
Create a new deploy key for this project Create a new deploy key for this project
= render @deploy_keys.form_partial_path = render @deploy_keys.form_partial_path
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- expanded = Rails.env.test? - expanded = Rails.env.test?
.project-edit-container .project-edit-container
%section.settings.general-settings %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
General project settings General project settings
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your project name, description, avatar, and other general settings. Update your project name, description, avatar, and other general settings.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.project-edit-errors .project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset %fieldset
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.sharing-permissions %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Permissions Permissions
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
%section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header .settings-header
%h4 %h4
Merge request settings Merge request settings
...@@ -83,14 +83,14 @@ ...@@ -83,14 +83,14 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Customize your merge request restrictions. Customize your merge request restrictions.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f = render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
= render 'export', project: @project = render 'export', project: @project
%section.settings.advanced-settings %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Advanced settings Advanced settings
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
.sub-section .sub-section
%h4 Housekeeping %h4 Housekeeping
%p %p
......
- @no_container = true - @no_container = true
- page_title "Contributors" - page_title _('Contributors')
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3') = webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs')
...@@ -7,23 +7,23 @@ ...@@ -7,23 +7,23 @@
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block .sub-header-block
.tree-ref-holder .tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs' = render 'shared/ref_switcher', destination: 'graphs'
%ul.breadcrumb.repo-breadcrumb = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
= commits_breadcrumbs
.loading-graph .loading-graph
.center .center
%h3.page-title %h3.page-title
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
Building repository graph. = s_('ContributorsPage|Building repository graph.')
%p.slead Please wait a moment, this page will automatically refresh when ready. %p.slead
= s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide .stat-graph.hide
.header.clearfix .header.clearfix
%h3#date_header.page-title %h3#date_header.page-title
%p.light %p.light
Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" } %input#brush_change{ :type => "hidden" }
.graphs.row .graphs.row
#contributors-master #contributors-master
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td %td
= truncate(hook_log.url, length: 50) = truncate(hook_log.url, length: 50)
%td.light %td.light
#{number_with_precision(hook_log.execution_duration, precision: 2)} ms #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light %td.light
= time_ago_with_tooltip(hook_log.created_at) = time_ago_with_tooltip(hook_log.created_at)
%td %td
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
- builds.select{|build| build.status == build_status}.each do |build| - builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to project_job_path(@project, build) do = link_to project_job_path(@project, build) do
= icon('arrow-right') = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" } %span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
%span %span
...@@ -100,4 +100,5 @@ ...@@ -100,4 +100,5 @@
- else - else
= build.id = build.id
- if build.retried? - if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
= sprite_icon('retry', size:16, css_class: 'icon-retry')
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) } %li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request - if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
......
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Protected Branches Protected Branches
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Keep stable branches secure and force developers to use merge requests. Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
%p %p
By default, protected branches are designed to: By default, protected branches are designed to:
%ul %ul
......
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Protected Tags Protected Tags
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Limit access to creating and updating tags. Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
%p %p
By default, protected tags are designed to: By default, protected tags are designed to:
%ul %ul
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings#js-general-pipeline-settings %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
General pipelines settings General pipelines settings
...@@ -12,10 +12,10 @@ ...@@ -12,10 +12,10 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= render 'projects/pipelines_settings/show' = render 'projects/pipelines_settings/show'
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Runners settings Runners settings
...@@ -23,10 +23,10 @@ ...@@ -23,10 +23,10 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Register and see your runners for this project. Register and see your runners for this project.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= render 'projects/runners/index' = render 'projects/runners/index'
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Secret variables Secret variables
...@@ -35,10 +35,10 @@ ...@@ -35,10 +35,10 @@
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= render "ci/variables/content" = render "ci/variables/content"
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= render 'ci/variables/index' = render 'ci/variables/index'
%section.settings %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
Pipeline triggers Pipeline triggers
...@@ -48,5 +48,5 @@ ...@@ -48,5 +48,5 @@
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
impersonate their associated user including their access to projects and their project impersonate their associated user including their access to projects and their project
permissions. permissions.
.settings-content.no-animate{ class: ('expanded' if expanded) } .settings-content
= render 'projects/triggers/index' = render 'projects/triggers/index'
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.stage-container.dropdown{ class: klass } .stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= custom_icon(icon_status) = sprite_icon(icon_status)
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= hook_log.trigger.singularize.titleize = hook_log.trigger.singularize.titleize
%p %p
%strong Elapsed time: %strong Elapsed time:
#{number_with_precision(hook_log.execution_duration, precision: 2)} ms #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%p %p
%strong Request time: %strong Request time:
= time_ago_with_tooltip(hook_log.created_at) = time_ago_with_tooltip(hook_log.created_at)
......
---
title: Tighten up whitelisting of certain Geo routes
merge_request: 15082
author:
type: fixed
---
title: Add a latest_merge_request_diff_id column to merge_requests
merge_request: 15035
author:
type: performance
---
title: Todos spelled correctly on Todos list page
merge_request: 15015
author:
type: changed
---
title: Fix webhooks recent deliveries
merge_request: 15146
author: Alexander Randa (@randaalex)
type: fixed
---
title: Add sudo scope for OAuth and Personal Access Tokens to be used by admins to
impersonate other users on the API
merge_request:
author:
type: added
---
title: Convert private tokens to Personal Access Tokens with sudo scope
merge_request:
author:
type: security
---
title: Remove private tokens from web interface and API
merge_request:
author:
type: security
---
title: Remove Session API now that private tokens are removed from user API endpoints
merge_request:
author:
type: removed
---
title: Fix cancel button not working while uploading on the new issue page
merge_request: 15137
author:
type: fixed
---
title: Remove Filesystem check metrics that use too much CPU to handle requests
merge_request:
author:
type: performance
---
title: Make NamespaceSelect change URL when filtering
merge_request: 14888
author:
type: fixed
---
title: Make contributors page translatable
merge_request: 14915
author:
type: other
...@@ -58,9 +58,10 @@ en: ...@@ -58,9 +58,10 @@ en:
expired: "The access token expired" expired: "The access token expired"
unknown: "The access token is invalid" unknown: "The access token is invalid"
scopes: scopes:
api: Access your API api: Access the authenticated user's API
read_user: Read user information read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
flash: flash:
applications: applications:
......
...@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do ...@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log get :audit_log
get :applications, to: 'oauth/applications#index' get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :reset_incoming_email_token put :reset_incoming_email_token
put :reset_rss_token put :reset_rss_token
put :update_username put :update_username
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# disable_ddl_transaction!
TOKEN_NAME = 'Private Token'.freeze
def up
execute <<~SQL
INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}'
FROM users
WHERE authentication_token IS NOT NULL
AND admin = FALSE
AND NOT EXISTS (
SELECT true
FROM personal_access_tokens
WHERE user_id = users.id
AND token = users.authentication_token
)
SQL
# Admins also need the `sudo` scope
execute <<~SQL
INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}'
FROM users
WHERE authentication_token IS NOT NULL
AND admin = TRUE
AND NOT EXISTS (
SELECT true
FROM personal_access_tokens
WHERE user_id = users.id
AND token = users.authentication_token
)
SQL
end
def down
if Gitlab::Database.postgresql?
execute <<~SQL
UPDATE users
SET authentication_token = pats.token
FROM (
SELECT user_id, token
FROM personal_access_tokens
WHERE name = '#{TOKEN_NAME}'
) AS pats
WHERE id = pats.user_id
SQL
else
execute <<~SQL
UPDATE users
INNER JOIN personal_access_tokens AS pats
ON users.id = pats.user_id
SET authentication_token = pats.token
WHERE pats.name = '#{TOKEN_NAME}'
SQL
end
execute <<~SQL
DELETE FROM personal_access_tokens
WHERE name = '#{TOKEN_NAME}'
AND EXISTS (
SELECT true
FROM users
WHERE id = personal_access_tokens.user_id
AND authentication_token = personal_access_tokens.token
)
SQL
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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