Commit 49cb7c39 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-06-07' into 'master'

CE Upstream - Wednesday

Closes #2563, #2411, gitlab-ce#19219, #2361, and gitlab-ce#31397

See merge request !2060
parents ae4bc9de 969bf3e3
......@@ -3,5 +3,8 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
......@@ -416,6 +416,7 @@ gitlab:assets:compile:
USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
script:
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
......
......@@ -278,6 +278,9 @@ group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
end
group :development do
......@@ -378,7 +381,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.7.0'
gem 'gitaly', '~> 0.8.0'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -296,7 +296,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.7.0)
gitaly (0.8.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -485,6 +485,7 @@ GEM
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
multi_json (1.12.1)
multi_xml (0.6.0)
......@@ -588,6 +589,8 @@ GEM
premailer-rails (1.9.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5)
mmap2 (~> 2.2.6)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
......@@ -966,7 +969,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.7.0)
gitaly (~> 0.8.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......@@ -1031,6 +1034,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
prometheus-client-mmap (~> 0.7.0.beta5)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 120" enable-background="new 0 0 12 120">
<path d="m12 6c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v108.09h2v-108.09c2.833-.479 5-2.943 5-5.91m-6 4c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/>
</svg>
......@@ -5,7 +5,8 @@ import Cookies from 'js-cookie';
class Activities {
constructor() {
Pager.init(20, true, false, this.updateTooltips);
Pager.init(20, true, false, data => data, this.updateTooltips);
$('.event-filter-link').on('click', (e) => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
......@@ -19,7 +20,7 @@ class Activities {
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips);
Pager.init(20, true, false, data => data, this.updateTooltips);
}
toggleFilter(sender) {
......
......@@ -57,6 +57,9 @@ export default {
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
......@@ -108,6 +111,7 @@ export default {
},
created() {
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -150,6 +154,7 @@ export default {
},
beforeDestroy() {
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
......@@ -160,9 +165,11 @@ export default {
v-if="loading">
<loading-icon />
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<transition name="slide-down">
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<ul
class="board-list"
v-show="!loading"
......
......@@ -52,6 +52,7 @@ export default {
this.error = true;
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
},
cancel() {
......@@ -79,6 +80,7 @@ export default {
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
......
......@@ -103,7 +103,7 @@ class List {
}
newIssue (issue) {
this.addIssue(issue);
this.addIssue(issue, null, 0);
this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
......@@ -111,6 +111,12 @@ class List {
const data = resp.json();
issue.id = data.iid;
issue.milestone = data.milestone;
})
.then(() => {
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
}
});
}
......
......@@ -22,6 +22,7 @@ gl.issueBoards.BoardsStore = {
create () {
this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&');
this.detail = { issue: {} };
},
createNewListDropdownData() {
this.state.currentBoard = {};
......
......@@ -64,7 +64,7 @@ window.Build = (function () {
$(window)
.off('resize.build')
.on('resize.build', this.sidebarOnResize.bind(this));
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.updateArtifactRemoveDate();
......@@ -250,6 +250,7 @@ window.Build = (function () {
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
......
......@@ -118,7 +118,7 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroyed() {
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
......
......@@ -7,6 +7,8 @@ window.CommitsList = (function() {
CommitsList.timer = null;
CommitsList.init = function(limit) {
this.$contentList = $('.content_list');
$("body").on("click", ".day-commits-table li.commit", function(e) {
if (e.target.nodeName !== "A") {
location.href = $(this).attr("url");
......@@ -14,9 +16,9 @@ window.CommitsList = (function() {
return false;
}
});
Pager.init(limit, false, false, function() {
gl.utils.localTimeAgo($('.js-timeago'));
});
Pager.init(limit, false, false, this.processCommits);
this.content = $("#commits-list");
this.searchField = $("#commits-search");
this.lastSearch = this.searchField.val();
......@@ -62,5 +64,34 @@ window.CommitsList = (function() {
});
};
// Prepare loaded data.
CommitsList.processCommits = (data) => {
let processedData = data;
const $processedData = $(processedData);
const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
let commitsCount;
// If commits headers show the same date,
// remove the last header and change the previous one.
if (lastShownDay === loadedShownDayFirst) {
// Last shown commits count under the last commits header.
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header.
processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
return processedData;
};
return CommitsList;
})();
......@@ -226,6 +226,16 @@ import AuditLogs from './audit_logs';
new gl.GLForm($('.tag-form'));
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new gl.GLForm($('.snippet-form'));
break;
case 'projects:releases:edit':
new ZenMode();
new gl.GLForm($('.release-form'));
......@@ -417,6 +427,9 @@ import AuditLogs from './audit_logs';
case 'users:show':
new UserCallout();
break;
case 'admin:conversational_development_index:show':
new UserCallout();
break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
......
......@@ -5,7 +5,7 @@ import './preview_markdown';
window.DropzoneInput = (function() {
function DropzoneInput(form) {
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile;
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm;
Dropzone.autoDiscover = false;
divHover = '<div class="div-dropzone-hover"></div>';
iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
......@@ -71,6 +71,7 @@ window.DropzoneInput = (function() {
pasteText(response.link.markdown, shouldPad);
// Show 'Attach a file' link only when all files have been uploaded.
if (!processingFileCount) $attachButton.removeClass('hide');
addFileToForm(response.link.url);
},
error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
// If 'error' event is fired by dropzone, the second parameter is error message.
......@@ -198,6 +199,10 @@ window.DropzoneInput = (function() {
return formTextarea.trigger('input');
};
addFileToForm = function(path) {
$(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">');
};
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
......
......@@ -120,7 +120,7 @@ export default {
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
......@@ -277,7 +277,7 @@ export default {
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
New environment
</a>
</div>
......
......@@ -69,7 +69,7 @@ export default {
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<ul class="dropdown-menu">
<li v-for="action in actions">
<button
type="button"
......
......@@ -422,8 +422,14 @@ export default {
};
</script>
<template>
<tr :class="{ 'js-child-row': model.isChildren }">
<td>
<div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Environment
</div>
<span
class="deploy-board-icon"
v-if="model.hasDeployBoard"
......@@ -439,13 +445,11 @@ export default {
class="fa fa-caret-down"
aria-hidden="true" />
</span>
<a
v-if="!model.isFolder"
class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
{{model.name}}
<span class="flex-truncate-child">{{model.name}}</span>
</a>
<span
v-else
......@@ -478,9 +482,9 @@ export default {
{{model.size}}
</span>
</span>
</td>
</div>
<td class="deployment-column">
<div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
......@@ -495,21 +499,26 @@ export default {
:tooltip-text="deploymentUser.username"
/>
</span>
</td>
</div>
<td class="environments-build-cell">
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a
v-if="shouldRenderBuildName"
class="build-link"
:href="buildPath">
{{buildName}}
</a>
</td>
</div>
<td>
<div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Commit
</div>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
class="js-commit-component">
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
......@@ -518,25 +527,30 @@ export default {
:title="commitTitle"
:author="commitAuthor"/>
</div>
<p
<div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
No deployments yet
</p>
</td>
</div>
</div>
<td>
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Updated
</div>
<span
v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
</td>
</div>
<td class="environments-actions">
<div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
<div
v-if="!model.isFolder"
class="btn-group pull-right"
class="btn-group environment-action-buttons"
role="group">
<actions-component
......@@ -570,6 +584,6 @@ export default {
:retry-url="retryUrl"
/>
</div>
</td>
</tr>
</div>
</div>
</template>
......@@ -19,7 +19,7 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
......
......@@ -43,7 +43,7 @@ export default {
<template>
<button
type="button"
class="btn"
class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">
......
......@@ -47,7 +47,7 @@ export default {
<template>
<button
type="button"
class="btn stop-env-link has-tooltip"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
......
......@@ -29,7 +29,7 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip"
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
......
......@@ -64,82 +64,72 @@ export default {
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="environments-name">
Environment
</th>
<th class="environments-deploy">
Last deployment
</th>
<th class="environments-build">
Job
</th>
<th class="environments-commit">
Commit
</th>
<th class="environments-date">
Updated
</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in environments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:toggleDeployBoard="toggleDeployBoard"
/>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="rowheader">
Environment
</div>
<div class="table-section section-10 environments-deploy" role="rowheader">
Deployment
</div>
<div class="table-section section-15 environments-build" role="rowheader">
Job
</div>
<div class="table-section section-25 environments-commit" role="rowheader">
Commit
</div>
<div class="table-section section-10 environments-date" role="rowheader">
Updated
</div>
</div>
<template
v-for="model in environments"
v-bind:model="model">
<div
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:toggleDeployBoard="toggleDeployBoard"
/>
<tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<td colspan="6" class="deploy-board-container">
<deploy-board
:store="store"
:service="service"
:environmentID="model.id"
:deployBoardData="model.deployBoardData"
:endpoint="model.rollout_status_path"
/>
</td>
</tr>
<div v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<div class="deploy-board-container">
<deploy-board
:store="store"
:service="service"
:environmentID="model.id"
:deployBoardData="model.deployBoardData"
:endpoint="model.rollout_status_path"
/>
</div>
</div>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6">
<loading-icon size="2" />
</td>
</tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="isLoadingFolderContent">
<loading-icon size="2" />
</div>
<template v-else>
<tr
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<template v-else>
<div
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<tr>
<td
colspan="6"
class="text-center">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
<div>
<div class="text-center prepend-top-10">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</div>
</div>
</template>
</template>
</tbody>
</table>
</template>
</div>
</template>
......@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (value && value.innerText) {
if (valueContainer && valueContainer.dataset.originalValue) {
valueText = valueContainer.dataset.originalValue;
} else if (value && value.innerText) {
valueText = value.innerText;
}
......
......@@ -109,11 +109,9 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.tokensContainer.addEventListener('click', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
......@@ -131,11 +129,9 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.tokensContainer.removeEventListener('click', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
......@@ -211,23 +207,13 @@ class FilteredSearchManager {
}
}
static selectToken(e) {
const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (!removeButtonSelected && button) {
e.preventDefault();
e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button);
}
}
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
e.stopPropagation();
// Prevent editToken from being triggered after token is removed
e.stopImmediatePropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
......@@ -249,10 +235,12 @@ class FilteredSearchManager {
editToken(e) {
const token = e.target.closest('.js-visual-token');
const sanitizedTokenName = token.querySelector('.name').textContent.trim();
const sanitizedTokenName = token && token.querySelector('.name').textContent.trim();
const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
if (token && canEdit) {
e.preventDefault();
e.stopPropagation();
gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange();
}
......
import AjaxCache from '~/lib/utils/ajax_cache';
import '~/flash'; /* global Flash */
import AjaxCache from '../lib/utils/ajax_cache';
import '../flash'; /* global Flash */
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
......@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
if (tokenValue === 'none') {
return Promise.resolve();
}
const username = tokenValue.replace(/^@/, '');
return UsersCache.retrieve(username)
.then((user) => {
if (!user) {
return;
}
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
${user.name}
`;
/* eslint-enable no-param-reassign */
})
// ignore error and leave username in the search bar
.catch(() => { });
}
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
tokenValueContainer.querySelector('.value').innerText = tokenValue;
const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
if (tokenName.toLowerCase() === 'label') {
const tokenType = tokenName.toLowerCase();
if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
} else if ((tokenType === 'author') || (tokenType === 'assignee')) {
FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
}
}
......@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return '';
const valueContainer = lastVisualToken.querySelector('.value-container');
const originalValue = valueContainer && valueContainer.dataset.originalValue;
if (originalValue) {
return originalValue;
}
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
......@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const nameElement = token.querySelector('.name');
let value;
if (token.classList.contains('filtered-search-token') && value) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText;
} else {
// token is a search term
input.value = name.innerText;
if (token.classList.contains('filtered-search-token')) {
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
const valueContainerElement = token.querySelector('.value-container');
value = valueContainerElement.dataset.originalValue;
if (!value) {
const valueElement = valueContainerElement.querySelector('.value');
value = valueElement.innerText;
}
}
// token is a search term
if (!value) {
value = nameElement.innerText;
}
input.value = value;
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
......
......@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut();
};
function Flash(message, type, parent) {
var flash, textDiv;
/**
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to Parent element under which Flash needs to appear
* @param {Object} actionConfig Map of config to show action on banner
* @param {String} href URL to which action link should point (default '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
*/
function Flash(message, type, parent, actionConfig) {
var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
......@@ -31,6 +44,23 @@ window.Flash = (function() {
text: message
});
textDiv.appendTo(flash);
if (actionConfig) {
const actionLinkConfig = {
class: 'flash-action',
href: actionConfig.href || '#',
text: actionConfig.title
};
if (!actionConfig.href) {
actionLinkConfig.role = 'button';
}
actionLink = $('<a/>', actionLinkConfig);
actionLink.appendTo(flash);
this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
}
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
}
......
......@@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
......@@ -35,6 +36,7 @@ class GfmAutoComplete {
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
$input.on('clear-commands-cache.atwho', () => this.clearCache());
});
}
......@@ -375,11 +377,14 @@ class GfmAutoComplete {
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else {
$.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => {
this.loadData($input, at, data);
}).fail(() => { this.isLoadingData[at] = false; });
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
}
}
loadData($input, at, data) {
this.isLoadingData[at] = false;
this.cachedData[at] = data;
......@@ -389,6 +394,10 @@ class GfmAutoComplete {
return $input.trigger('keyup');
}
clearCache() {
this.cachedData = {};
}
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
......
......@@ -31,9 +31,13 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
event.stopPropagation();
const $form = $(event.currentTarget);
if (!$form.attr('novalidate')) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
}
}
......
/* eslint-disable no-new */
import IntegrationSettingsForm from './integration_settings_form';
$(() => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
});
/* global Flash */
export default class IntegrationSettingsForm {
constructor(formSelector) {
this.$form = $(formSelector);
// Form Metadata
this.canTestService = this.$form.data('can-test');
this.testEndPoint = this.$form.data('test-url');
// Form Child Elements
this.$serviceToggle = this.$form.find('#service_active');
this.$submitBtn = this.$form.find('button[type="submit"]');
this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
}
init() {
// Initialize View
this.toggleServiceState(this.$serviceToggle.is(':checked'));
// Bind Event Listeners
this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
this.$submitBtn.on('click', e => this.handleSettingsSave(e));
}
handleSettingsSave(e) {
// Check if Service is marked active, as if not marked active,
// We can skip testing it and directly go ahead to allow form to
// be submitted
if (!this.$serviceToggle.is(':checked')) {
return;
}
// Service was marked active so now we check;
// 1) If form contents are valid
// 2) If this service can be tested
// If both conditions are true, we override form submission
// and test the service using provided configuration.
if (this.$form.get(0).checkValidity() && this.canTestService) {
e.preventDefault();
this.testSettings(this.$form.serialize());
}
}
handleServiceToggle(e) {
this.toggleServiceState($(e.currentTarget).is(':checked'));
}
/**
* Change Form's validation enforcement based on service status (active/inactive)
*/
toggleServiceState(serviceActive) {
this.toggleSubmitBtnLabel(serviceActive);
if (serviceActive) {
this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) {
this.$form.attr('novalidate', 'novalidate');
}
}
/**
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
let btnLabel = 'Save changes';
if (serviceActive && this.canTestService) {
btnLabel = 'Test settings and save changes';
}
this.$submitBtnLabel.text(btnLabel);
}
/**
* Toggle Submit button state based on provided boolean value of `saveTestActive`
* When enabled, it does two things, and reverts back when disabled
*
* 1. It shows load spinner on submit button
* 2. Makes submit button disabled
*/
toggleSubmitBtnState(saveTestActive) {
if (saveTestActive) {
this.$submitBtn.disable();
this.$submitBtnLoader.removeClass('hidden');
} else {
this.$submitBtn.enable();
this.$submitBtnLoader.addClass('hidden');
}
}
/* eslint-disable promise/catch-or-return, no-new */
/**
* Test Integration config
*/
testSettings(formData) {
this.toggleSubmitBtnState(true);
$.ajax({
type: 'PUT',
url: this.testEndPoint,
data: formData,
})
.done((res) => {
if (res.error) {
new Flash(res.message, null, null, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
this.$form.submit();
},
});
} else {
this.$form.submit();
}
})
.fail(() => {
new Flash('Something went wrong on our end.');
})
.always(() => {
this.toggleSubmitBtnState(false);
});
}
}
......@@ -7,6 +7,7 @@ import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import '../../lib/utils/url_utility';
......@@ -50,6 +51,21 @@ export default {
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
issuableTemplates: {
type: Array,
required: false,
......@@ -86,6 +102,9 @@ export default {
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
});
return {
......@@ -98,10 +117,14 @@ export default {
formState() {
return this.store.formState;
},
hasUpdated() {
return !!this.state.updatedAt;
},
},
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
},
methods: {
......@@ -240,6 +263,11 @@ export default {
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath" />
</div>
</div>
</template>
......@@ -16,11 +16,6 @@
type: String,
required: true,
},
updatedAt: {
type: String,
required: false,
default: '',
},
taskStatus: {
type: String,
required: false,
......@@ -31,7 +26,6 @@
return {
preAnimation: false,
pulseAnimation: false,
timeAgoEl: $('.js-issue-edited-ago'),
};
},
watch: {
......@@ -39,12 +33,6 @@
this.animateChange();
this.$nextTick(() => {
const toolTipTime = gl.utils.formatDate(this.updatedAt);
this.timeAgoEl.attr('datetime', this.updatedAt)
.attr('title', toolTipTime)
.tooltip('fixTitle');
this.renderGFM();
});
},
......
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
props: {
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
},
components: {
timeAgoTooltip,
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
},
};
</script>
<template>
<small
class="edited-text"
>
Edited
<time-ago-tooltip
v-if="updatedAt"
placement="bottom"
:time="updatedAt"
/>
<span
v-if="hasUpdatedBy"
>
by
<a
class="author_link"
:href="updatedByPath"
>
<span>{{updatedByName}}</span>
</a>
</span>
</small>
</template>
......@@ -42,6 +42,9 @@ document.addEventListener('DOMContentLoaded', () => {
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
},
});
},
......
......@@ -4,6 +4,9 @@ export default class Store {
titleText,
descriptionHtml,
descriptionText,
updatedAt,
updatedByName,
updatedByPath,
}) {
this.state = {
titleHtml,
......@@ -11,7 +14,9 @@ export default class Store {
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt: '',
updatedAt,
updatedByName,
updatedByPath,
};
this.formState = {
title: '',
......@@ -30,6 +35,8 @@ export default class Store {
this.state.descriptionText = data.description_text;
this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at;
this.state.updatedByName = data.updated_by_name;
this.state.updatedByPath = data.updated_by_path;
}
stateShouldUpdate(data) {
......
......@@ -6,8 +6,8 @@ class AjaxCache extends Cache {
this.pendingRequests = { };
}
retrieve(endpoint) {
if (this.hasData(endpoint)) {
retrieve(endpoint, forceRetrieve) {
if (this.hasData(endpoint) && !forceRetrieve) {
return Promise.resolve(this.get(endpoint));
}
......
var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Pipeline Health":["流水线健康指标"],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Showing %d event":["显示 %d 个事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":[""],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":[""]}}};
\ No newline at end of file
var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":[""],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":[""]}}};
\ No newline at end of file
var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["送交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":[""],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":[""]}}};
\ No newline at end of file
......@@ -16,6 +16,7 @@ import autosize from 'vendor/autosize';
import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle';
import './autosave';
import './dropzone_input';
......@@ -66,7 +67,6 @@ const normalizeNewlines = function(str) {
this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.flashErrors = [];
this.cleanBinding();
this.addBinding();
......@@ -325,6 +325,9 @@ const normalizeNewlines = function(str) {
if (Notes.isNewNote(noteEntity, this.note_ids)) {
this.note_ids.push(noteEntity.id);
if ($notesList.length) {
$notesList.find('.system-note.being-posted').remove();
}
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
this.setupNewNote($newNote);
......@@ -1118,12 +1121,14 @@ const normalizeNewlines = function(str) {
};
Notes.prototype.addFlash = function(...flashParams) {
this.flashErrors.push(new Flash(...flashParams));
this.flashInstance = new Flash(...flashParams);
};
Notes.prototype.clearFlash = function() {
this.flashErrors.forEach(flash => flash.flashContainer.remove());
this.flashErrors = [];
if (this.flashInstance && this.flashInstance.flashContainer) {
this.flashInstance.flashContainer.hide();
this.flashInstance = null;
}
};
Notes.prototype.cleanForm = function($form) {
......@@ -1187,7 +1192,7 @@ const normalizeNewlines = function(str) {
Notes.prototype.getFormData = function($form) {
return {
formData: $form.serialize(),
formContent: $form.find('.js-note-text').val(),
formContent: _.escape($form.find('.js-note-text').val()),
formAction: $form.attr('action'),
};
};
......@@ -1206,20 +1211,47 @@ const normalizeNewlines = function(str) {
return formContent.replace(REGEX_SLASH_COMMANDS, '').trim();
};
/**
* Gets appropriate description from slash commands found in provided `formContent`
*/
Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) {
let tempFormContent;
// Identify executed slash commands from `formContent`
const executedCommands = availableSlashCommands.filter((command, index) => {
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(formContent);
});
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
tempFormContent = 'Applying multiple commands';
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
tempFormContent = `Applying command to ${commandDescription}`;
}
} else {
tempFormContent = 'Applying command';
}
return tempFormContent;
};
/**
* Create placeholder note DOM element populated with comment body
* that we will show while comment is being posted.
* Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element.
*/
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<a href="/${currentUsername}"><span class="avatar dummy-avatar"></span></a>
<a href="/${currentUsername}">
<img class="avatar s40" src="${currentUserAvatar}">
</a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
......@@ -1232,7 +1264,7 @@ const normalizeNewlines = function(str) {
</div>
<div class="note-body">
<div class="note-text">
<p>${escapedFormContent}</p>
<p>${formContent}</p>
</div>
</div>
</div>
......@@ -1243,6 +1275,23 @@ const normalizeNewlines = function(str) {
return $tempNote;
};
/**
* Create Placeholder System Note DOM element populated with slash command description
*/
Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) {
const $tempNote = $(
`<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half">
<div class="timeline-entry-inner">
<div class="timeline-content">
<i>${formContent}</i>
</div>
</div>
</li>`
);
return $tempNote;
};
/**
* This method does following tasks step-by-step whenever a new comment
* is submitted by user (both main thread comments as well as discussion comments).
......@@ -1274,7 +1323,9 @@ const normalizeNewlines = function(str) {
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction } = this.getFormData($form);
const uniqueId = _.uniqueId('tempNote_');
let noteUniqueId;
let systemNoteUniqueId;
let hasSlashCommands = false;
let $notesContainer;
let tempFormContent;
......@@ -1295,16 +1346,28 @@ const normalizeNewlines = function(str) {
tempFormContent = formContent;
if (this.hasSlashCommands(formContent)) {
tempFormContent = this.stripSlashCommands(formContent);
hasSlashCommands = true;
}
// Show placeholder note
if (tempFormContent) {
// Show placeholder note
noteUniqueId = _.uniqueId('tempNote_');
$notesContainer.append(this.createPlaceholderNote({
formContent: tempFormContent,
uniqueId,
uniqueId: noteUniqueId,
isDiscussionNote,
currentUsername: gon.current_username,
currentUserFullname: gon.current_user_fullname,
currentUserAvatar: gon.current_user_avatar_url,
}));
}
// Show placeholder system note
if (hasSlashCommands) {
systemNoteUniqueId = _.uniqueId('tempSystemNote_');
$notesContainer.append(this.createPlaceholderSystemNote({
formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)),
uniqueId: systemNoteUniqueId,
}));
}
......@@ -1322,7 +1385,13 @@ const normalizeNewlines = function(str) {
gl.utils.ajaxPost(formAction, formData)
.then((note) => {
// Submission successful! remove placeholder
$notesContainer.find(`#${uniqueId}`).remove();
$notesContainer.find(`#${noteUniqueId}`).remove();
// Reset cached commands list when command is applied
if (hasSlashCommands) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
this.clearFlashWrapper();
......@@ -1359,7 +1428,11 @@ const normalizeNewlines = function(str) {
$form.trigger('ajax:success', [note]);
}).fail(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${uniqueId}`).remove();
$notesContainer.find(`#${noteUniqueId}`).remove();
if (hasSlashCommands) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove();
}
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
......
......@@ -6,11 +6,12 @@ import '~/lib/utils/url_utility';
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']);
this.limit = limit;
this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
this.prepareData = prepareData;
this.callback = callback;
this.loading = $('.loading').first();
if (preload) {
......@@ -29,7 +30,7 @@ import '~/lib/utils/url_utility';
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
this.append(data.count, data.html);
this.append(data.count, this.prepareData(data.html));
this.callback();
// keep loading until we've filled the viewport height
......
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'PipelineHeaderSection',
props: {
pipeline: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
components: {
ciHeader,
loadingIcon,
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.pipeline.details && this.pipeline.details.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length;
},
},
methods: {
postAction(action) {
const index = this.actions.indexOf(action);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerPostAction', action);
},
getActions() {
const actions = [];
if (this.pipeline.retry_path) {
actions.push({
label: 'Retry',
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false,
});
}
if (this.pipeline.cancel_path) {
actions.push({
label: 'Cancel running',
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
isLoading: false,
});
}
return actions;
},
},
watch: {
pipeline() {
this.actions = this.getActions();
},
},
};
</script>
<template>
<div class="pipeline-header-container">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Pipeline"
:item-id="pipeline.id"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
@actionClicked="postAction"
/>
<loading-icon
v-else
size="2"/>
</div>
</template>
......@@ -33,7 +33,7 @@ export default {
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:link-href="pipeline.user.path"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
......
/* global Flash */
import Vue from 'vue';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
......@@ -9,7 +13,8 @@ document.addEventListener('DOMContentLoaded', () => {
mediator.fetchPipeline();
const pipelineGraphApp = new Vue({
// eslint-disable-next-line
new Vue({
el: '#js-pipeline-graph-vue',
data() {
return {
......@@ -29,5 +34,37 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
return pipelineGraphApp;
// eslint-disable-next-line
new Vue({
el: '#js-pipeline-header-vue',
data() {
return {
mediator,
};
},
components: {
pipelineHeader,
},
created() {
eventHub.$on('headerPostAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction);
},
methods: {
postAction(action) {
this.mediator.service.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => new Flash('An error occurred while making the request.'));
},
},
render(createElement) {
return createElement('pipeline-header', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
},
});
},
});
});
......@@ -26,6 +26,8 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
} else {
this.refreshPipeline();
}
Visibility.change(() => {
......@@ -48,4 +50,10 @@ export default class pipelinesMediator {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
}
refreshPipeline() {
this.service.getPipeline()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
}
......@@ -169,7 +169,7 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroyed() {
beforeDestroy() {
eventHub.$off('refreshPipelines');
},
......
......@@ -11,4 +11,9 @@ export default class PipelineService {
getPipeline() {
return this.pipeline.get();
}
// eslint-disable-next-line
postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`);
}
}
......@@ -33,8 +33,6 @@ export default class PipelinesService {
/**
* Post request for all pipelines actions.
* Endpoint content type needs to be:
* `Content-Type:application/x-www-form-urlencoded`
*
* @param {String} endpoint
* @return {Promise}
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
(function() {
this.ProjectNew = (function() {
function ProjectNew() {
......@@ -7,6 +12,7 @@
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$enableApprovers = $('.js-require-approvals-toggle');
this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
......@@ -32,6 +38,42 @@
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
let projectVisibility = $visibilitySelect.val();
const PROJECT_VISIBILITY_PRIVATE = '0';
$visibilitySelect.on('change', () => {
const newProjectVisibility = $visibilitySelect.val();
if (projectVisibility !== newProjectVisibility) {
this.$projectSelects.each((idx, select) => {
const $select = $(select);
const $options = $select.find('option');
const values = $.map($options, e => e.value);
// if switched to "private", limit visibility options
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
if ($select.val() !== values[0] && $select.val() !== values[1]) {
$select.val(values[1]).trigger('change');
highlightChanges($select);
}
$options.slice(2).disable();
}
// if switched from "private", increase visibility for non-disabled options
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
$options.enable();
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
$select.val(values[values.length - 1]).trigger('change');
highlightChanges($select);
}
}
});
projectVisibility = newProjectVisibility;
}
});
};
ProjectNew.prototype.toggleApproverSettingsVisibility = function(e) {
......@@ -67,8 +109,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
......@@ -82,29 +126,40 @@
var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
$this.find('option').enable();
if (selectedVal < repoSelectVal) {
$this.val(selectedVal);
if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal).trigger('change');
highlightChanges($this);
}
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
$this.find("option[value='" + selectedVal + "']").nextAll().disable();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
prevSelectedVal = selectedVal;
}.bind(this));
};
......
import Cookies from 'js-cookie';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
export default class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $('.user-callout');
constructor(className = 'user-callout') {
this.userCalloutBody = $(`.${className}`);
this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName);
this.init();
}
......@@ -18,7 +17,7 @@ export default class UserCallout {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
Cookies.set(this.cookieName, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
......
......@@ -91,7 +91,7 @@ export default {
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.web_url &&
this.author.path &&
this.author.username;
},
......@@ -135,12 +135,12 @@ export default {
{{shortSha}}
</a>
<p class="commit-title">
<span v-if="title">
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
:link-href="author.web_url"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
......@@ -153,7 +153,7 @@ export default {
<span v-else>
Cant find HEAD commit for this branch
</span>
</p>
</div>
</div>
`,
};
<script>
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
......@@ -31,7 +32,8 @@ export default {
},
user: {
type: Object,
required: true,
required: false,
default: () => ({}),
},
actions: {
type: Array,
......@@ -46,8 +48,9 @@ export default {
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarLink,
userAvatarImage,
},
computed: {
......@@ -58,13 +61,13 @@ export default {
methods: {
onClickAction(action) {
this.$emit('postAction', action);
this.$emit('actionClicked', action);
},
},
};
</script>
<template>
<header class="page-content-header top-area">
<header class="page-content-header">
<section class="header-main-content">
<ci-icon-badge :status="status" />
......@@ -79,21 +82,23 @@ export default {
by
<user-avatar-link
:link-href="user.web_url"
:img-src="user.avatar_url"
:img-alt="userAvatarAltText"
:tooltip-text="user.name"
:img-size="24"
/>
<a
:href="user.web_url"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
{{user.name}}
</a>
<template v-if="user">
<a
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
<user-avatar-image
:img-src="user.avatar_url"
:img-alt="userAvatarAltText"
:tooltip-text="user.name"
:img-size="24"
/>
{{user.name}}
</a>
</template>
</section>
<section
......@@ -111,11 +116,17 @@ export default {
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
:disabled="action.isLoading"
:class="action.cssClass"
type="button">
{{action.label}}
</button>
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
aria-hidden="true">
</i>
</button>
</template>
</section>
</header>
......
......@@ -83,7 +83,7 @@ export default {
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
web_url: `mailto:${this.pipeline.commit.author_email}`,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
......
......@@ -60,6 +60,12 @@ export default {
avatarSizeClass() {
return `s${this.size}`;
},
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside user avatar link.
// In both cases we should render the defaultAvatarUrl
imageSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
},
};
</script>
......@@ -68,7 +74,7 @@ export default {
<img
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imgSrc"
:src="imageSource"
:width="size"
:height="size"
:alt="imgAlt"
......
......@@ -49,3 +49,4 @@
@import "framework/icons.scss";
@import "framework/snippets.scss";
@import "framework/memory_graph.scss";
@import "framework/responsive-tables.scss";
......@@ -85,7 +85,7 @@
}
/**
* Blame file
* Annotate file
*/
&.blame {
table {
......
......@@ -84,6 +84,7 @@
.filtered-search-term {
display: -webkit-flex;
display: flex;
flex-shrink: 0;
margin-top: 5px;
margin-bottom: 5px;
......@@ -141,15 +142,17 @@
}
}
}
}
.selected {
.name {
background-color: $filter-name-selected-color;
}
.filtered-search-token:hover,
.filtered-search-token .selected,
.filtered-search-term .selected {
.name {
background-color: $filter-name-selected-color;
}
.value-container {
background-color: $filter-value-selected-color;
}
.value-container {
background-color: $filter-value-selected-color;
}
}
......@@ -233,7 +236,7 @@
width: 35px;
background-color: $white-light;
border: none;
position: absolute;
position: static;
right: 0;
height: 100%;
outline: none;
......
......@@ -16,6 +16,22 @@
@extend .alert;
@extend .alert-danger;
margin: 0;
.flash-text,
.flash-action {
display: inline-block;
}
a.flash-action {
margin-left: 5px;
text-decoration: none;
font-weight: normal;
border-bottom: 1px solid;
&:hover {
border-color: transparent;
}
}
}
.flash-notice,
......
......@@ -108,11 +108,6 @@
}
}
.issue-edited-ago,
.note_edited_ago {
display: none;
}
aside:not(.right-sidebar) {
display: none;
}
......
@mixin flex-max-width($max) {
flex: 0 0 #{$max + '%'};
max-width: #{$max + '%'};
}
.gl-responsive-table-row {
margin-top: 10px;
border: 1px solid $border-color;
@media (min-width: $screen-md-min) {
padding: 15px 0;
margin: 0;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid $white-normal;
}
.table-section {
white-space: nowrap;
.branch-commit {
max-width: 100%;
}
$section-widths: 10 15 20 25 30 40;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
@media (min-width: $screen-md-min) {
max-width: #{$width + '%'};
}
}
}
&:not(.table-button-footer) {
@media (max-width: $screen-sm-max) {
display: flex;
align-self: stretch;
padding: 10px;
align-items: center;
height: 62px;
&:not(:first-of-type) {
border-top: 1px solid $white-normal;
}
}
}
}
}
.table-row-header {
font-size: 13px;
@media (max-width: $screen-sm-max) {
display: none;
}
}
.table-mobile-header {
color: $gl-text-color-secondary;
@include flex-max-width(40);
@media (min-width: $screen-md-min) {
display: none;
}
}
.table-mobile-content {
@media (max-width: $screen-sm-max) {
@include flex-max-width(60);
text-align: right;
}
}
.flex-truncate-parent {
display: flex;
}
.flex-truncate-child {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: $screen-md-min) {
flex: 0 0 90%;
}
}
......@@ -188,10 +188,12 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54;
$ldap-members-override-bg: $orange-50;
/*
* Common component specific colors
*/
......@@ -589,3 +591,10 @@ Cross-project Pipelines
$linked-project-column-margin: 60px;
/*
Convdev Index
*/
$color-high-score: $green-400;
$color-average-score: $orange-400;
$color-low-score: $red-400;
......@@ -204,21 +204,53 @@
}
}
.slide-down-enter {
transform: translateY(-100%);
}
.slide-down-enter-active {
transition: transform $fade-in-duration;
+ .board-list {
transform: translateY(-136px);
transition: none;
}
}
.slide-down-enter-to {
+ .board-list {
transform: translateY(0);
transition: transform $fade-in-duration ease;
}
}
.slide-down-leave {
transform: translateY(0);
}
.slide-down-leave-active {
transition: all $fade-in-duration;
transform: translateY(-136px);
+ .board-list {
transition: transform $fade-in-duration ease;
transform: translateY(-136px);
}
}
.board-list-component {
height: calc(100% - 49px);
overflow: hidden;
}
.board-list {
height: 100%;
width: 100%;
margin-bottom: 0;
padding: 5px;
list-style: none;
overflow-y: scroll;
overflow-x: hidden;
&.is-smaller {
height: calc(100% - 136px);
}
}
.board-list-loading {
......@@ -379,6 +411,7 @@
}
.board-new-issue-form {
z-index: 1;
margin: 5px;
}
......
$space-between-cards: 8px;
.convdev-empty svg {
margin: 64px auto 32px;
max-width: 420px;
}
.convdev-header {
margin-top: $gl-padding;
margin-bottom: $gl-padding;
padding: 0 4px;
display: flex;
align-items: center;
.convdev-header-title {
font-size: 48px;
line-height: 1;
margin: 0;
}
.convdev-header-subtitle {
font-size: 22px;
line-height: 1;
color: $gl-text-color-secondary;
margin-left: 8px;
font-weight: 500;
a {
font-size: 18px;
color: $gl-text-color-secondary;
&:hover {
color: $blue-500;
}
}
}
}
.convdev-cards {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.convdev-card-wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: center;
width: 50%;
border-color: $border-color;
margin: 0 0 32px;
padding: $space-between-cards / 2;
position: relative;
@media (min-width: $screen-xs-min) {
width: percentage(1 / 4);
}
@media (min-width: $screen-sm-min) {
width: percentage(1 / 5);
}
@media (min-width: $screen-md-min) {
width: percentage(1 / 6);
}
@media (min-width: $screen-lg-min) {
width: percentage(1 / 10);
}
}
.convdev-card {
border: solid 1px $border-color;
border-radius: 3px;
border-top-width: 3px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.convdev-card-low {
border-top-color: $color-low-score;
.card-score-big {
background-color: $red-25;
}
}
.convdev-card-average {
border-top-color: $color-average-score;
.card-score-big {
background-color: $orange-25;
}
}
.convdev-card-high {
border-top-color: $color-high-score;
.card-score-big {
background-color: $green-25;
}
}
.convdev-card-title {
margin: $gl-padding auto auto;
max-width: 100px;
h3 {
font-size: 14px;
margin: 0 0 2px;
}
.text-light {
font-size: 13px;
line-height: 1.25;
color: $gl-text-color-secondary;
}
}
.card-scores {
display: flex;
justify-content: space-around;
align-items: center;
margin: $gl-padding $gl-btn-padding;
line-height: 1;
}
.card-score {
color: $gl-text-color-secondary;
.card-score-name {
font-size: 13px;
margin-top: 4px;
}
}
.card-score-value {
font-size: 16px;
color: $gl-text-color;
font-weight: 500;
}
.card-score-big {
border-top: 2px solid $border-color;
border-bottom: 1px solid $border-color;
font-size: 22px;
padding: 10px 0;
font-weight: 500;
}
.card-buttons {
display: flex;
> * {
font-size: 16px;
color: $gl-text-color-secondary;
padding: 10px;
flex-grow: 1;
&:hover {
background-color: $border-color;
color: $gl-text-color;
}
+ * {
border-left: solid 1px $border-color;
}
}
}
.convdev-steps {
margin-top: $gl-padding;
height: 1px;
min-width: 100%;
justify-content: space-around;
position: relative;
background: $border-color;
}
.convdev-step {
$step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%;
@each $pos in $step-positions {
$i: index($step-positions, $pos);
&:nth-child(#{$i}) {
left: $pos;
}
}
position: absolute;
transform-origin: 75% 50%;
padding: 8px;
height: 50px;
width: 50px;
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: center;
border: solid 1px $border-color;
background: $white-light;
transform: translate(-50%, -50%);
color: $gl-text-color-secondary;
fill: $gl-text-color-secondary;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&:hover {
padding: 8px 10px;
fill: currentColor;
z-index: 100;
height: auto;
width: auto;
.convdev-step-title {
max-height: 2em;
opacity: 1;
transition: opacity 0.2s;
}
svg {
transform: scale(1.5);
margin: $gl-btn-padding;
}
}
svg {
transition: transform 0.1s;
width: 30px;
height: 30px;
min-height: 30px;
min-width: 30px;
}
}
.convdev-step-title {
max-height: 0;
opacity: 0;
text-transform: uppercase;
margin: $gl-vert-padding 0 0;
text-align: center;
font-size: 12px;
}
.convdev-high-score {
color: $color-high-score;
}
.convdev-average-score {
color: $color-average-score;
}
.convdev-low-score {
color: $color-low-score;
}
......@@ -11,34 +11,7 @@
}
.environments-container {
.table-holder {
width: 100%;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
}
.table.ci-table {
.environments-actions {
min-width: 300px;
}
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-date {
width: 10%;
}
.environments-name,
.environments-deploy,
.environments-build {
width: 15%;
}
.ci-table {
.deployment-column {
> span {
word-break: break-all;
......@@ -300,6 +273,54 @@
}
}
.gl-responsive-table-row {
.environments-actions {
@media (min-width: $screen-md-min) {
text-align: right;
}
@media (max-width: $screen-sm-max) {
background-color: $gray-normal;
align-self: stretch;
border-top: 1px solid $border-color;
.environment-action-buttons {
padding: 10px;
display: flex;
.btn {
border-radius: 3px;
}
> .btn-group,
.external-url,
.btn {
flex: 1;
flex-basis: 28px;
}
.dropdown-new {
width: 100%;
}
}
}
}
.branch-commit {
max-width: 100%;
}
}
.folder-row {
padding: 15px 0;
border-bottom: 1px solid $white-normal;
@media (max-width: $screen-sm-max) {
border-top: 1px solid $white-normal;
margin-top: 10px;
}
}
.prometheus-graph {
text {
fill: $gl-text-color;
......
......@@ -111,13 +111,28 @@
margin-top: 0;
text-align: center;
font-size: 12px;
align-items: center;
@media (max-width: $screen-sm-max) {
@media (max-width: $screen-md-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
-webkit-order: 4;
margin: 6px auto;
width: 100%;
}
.fa {
margin-right: 8px;
}
}
.right-sidebar-expanded {
.confidential-issue-warning {
// When the sidebar is open the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
......
......@@ -1197,3 +1197,12 @@
font-size: 10px;
vertical-align: top;
}
.pipeline-header-container {
min-height: 55px;
.text-center {
padding-top: 12px;
}
}
......@@ -287,6 +287,7 @@ table.u2f-registrations {
.user-callout {
margin: 0 auto;
max-width: $screen-lg-min;
.bordered-box {
border: 1px solid $blue-300;
......@@ -295,14 +296,15 @@ table.u2f-registrations {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.landing {
margin-top: $gl-padding;
margin-bottom: $gl-padding;
padding: 32px;
.close {
position: absolute;
top: 20px;
right: 20px;
opacity: 1;
......@@ -330,11 +332,20 @@ table.u2f-registrations {
height: 110px;
vertical-align: top;
}
&.convdev {
margin: 0 0 0 30px;
svg {
height: 127px;
}
}
}
.user-callout-copy {
display: inline-block;
vertical-align: top;
max-width: 570px;
}
}
......@@ -348,12 +359,20 @@ table.u2f-registrations {
.landing {
.svg-container,
.user-callout-copy {
margin: 0;
margin: 0 auto;
display: block;
svg {
height: 75px;
}
&.convdev {
margin: $gl-padding auto 0;
svg {
height: 120px;
}
}
}
}
}
......
......@@ -29,6 +29,20 @@
& > .form-group {
padding-left: 0;
}
select option[disabled] {
display: none;
}
}
select {
background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
}
.help-block {
......
......@@ -150,6 +150,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled,
:terminal_max_session_time,
:polling_interval_multiplier,
:prometheus_metrics_enabled,
:usage_ping_enabled,
disabled_oauth_sign_in_sources: [],
......
......@@ -39,7 +39,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
def destroy
@application.destroy
redirect_to admin_applications_url, notice: 'Application was successfully destroyed.'
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end
private
......
class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController
def show
@metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
end
end
......@@ -23,7 +23,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
deploy_key.destroy
respond_to do |format|
format.html { redirect_to admin_deploy_keys_path }
format.html { redirect_to admin_deploy_keys_path, status: 302 }
format.json { head :ok }
end
end
......
......@@ -25,7 +25,7 @@ class Admin::GeoNodesController < Admin::ApplicationController
def destroy
@node.destroy
redirect_to admin_geo_nodes_path, notice: 'Node was successfully removed.'
redirect_to admin_geo_nodes_path, status: 302, notice: 'Node was successfully removed.'
end
def repair
......
......@@ -43,19 +43,22 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
status = Members::CreateService.new(@group, current_user, params).execute
member_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute
if status
if result[:status] == :success
redirect_to [:admin, @group], notice: 'Users were successfully added.'
else
redirect_to [:admin, @group], alert: 'No users specified.'
redirect_to [:admin, @group], alert: result[:message]
end
end
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
redirect_to admin_groups_path,
status: 302,
alert: "Group '#{@group.name}' was scheduled for deletion."
end
private
......
......@@ -34,7 +34,7 @@ class Admin::HooksController < Admin::ApplicationController
def destroy
hook.destroy
redirect_to admin_hooks_path
redirect_to admin_hooks_path, status: 302
end
def test
......
......@@ -36,9 +36,9 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
redirect_to admin_user_identities_path(@user), status: 302, notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
redirect_to admin_user_identities_path(@user), status: 302, alert: 'Failed to remove user identity.'
end
end
......
......@@ -11,7 +11,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
session[:impersonator_id] = nil
redirect_to admin_user_path(original_user)
redirect_to admin_user_path(original_user), status: 302
end
private
......
......@@ -15,9 +15,9 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format|
if key.destroy
format.html { redirect_to [:admin, user], notice: 'User key was successfully removed.' }
format.html { redirect_to keys_admin_user_path(user), status: 302, notice: 'User key was successfully removed.' }
else
format.html { redirect_to [:admin, user], alert: 'Failed to remove user key.' }
format.html { redirect_to keys_admin_user_path(user), status: 302, alert: 'Failed to remove user key.' }
end
end
end
......
......@@ -41,7 +41,7 @@ class Admin::LabelsController < Admin::ApplicationController
respond_to do |format|
format.html do
redirect_to(admin_labels_path, notice: 'Label was removed')
redirect_to admin_labels_path, status: 302, notice: 'Label was removed'
end
format.js
end
......
......@@ -43,7 +43,7 @@ class Admin::LicensesController < Admin::ApplicationController
flash[:alert] = "The license was removed. GitLab now no longer has a valid license."
end
redirect_to admin_license_path
redirect_to admin_license_path, status: 302
end
private
......
......@@ -18,7 +18,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
runner = rp.runner
rp.destroy
redirect_to admin_runner_path(runner)
redirect_to admin_runner_path(runner), status: 302
end
private
......
......@@ -27,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
def destroy
@runner.destroy
redirect_to admin_runners_path
redirect_to admin_runners_path, status: 302
end
def resume
......
......@@ -8,7 +8,9 @@ class Admin::SpamLogsController < Admin::ApplicationController
if params[:remove_user]
spam_log.remove_user(deleted_by: current_user)
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
redirect_to admin_spam_logs_path,
status: 302,
notice: "User #{spam_log.user.username} was successfully removed."
else
spam_log.destroy
head :ok
......
......@@ -138,10 +138,10 @@ class Admin::UsersController < Admin::ApplicationController
end
def destroy
DeleteUserWorker.perform_async(current_user.id, user.id)
user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete))
respond_to do |format|
format.html { redirect_to admin_users_path, notice: "The user is being deleted." }
format.html { redirect_to admin_users_path, status: 302, notice: "The user is being deleted." }
format.json { head :ok }
end
end
......
......@@ -2,14 +2,15 @@ module MembershipActions
extend ActiveSupport::Concern
def create
status = Members::CreateService.new(membershipable, current_user, params).execute
create_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(membershipable, current_user, create_params).execute
redirect_url = members_page_url
if status
if result[:status] == :success
redirect_to redirect_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: 'No users specified.'
redirect_to redirect_url, alert: result[:message]
end
end
......
......@@ -15,7 +15,11 @@ class Dashboard::TodosController < Dashboard::ApplicationController
TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.html do
redirect_to dashboard_todos_path,
status: 302,
notice: 'Todo was successfully marked as done.'
end
format.js { head :ok }
format.json { render json: todos_counts }
end
......@@ -25,7 +29,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.html { redirect_to dashboard_todos_path, status: 302, notice: 'All todos were marked as done.' }
format.js { head :ok }
format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
......
......@@ -5,6 +5,6 @@ class Groups::AvatarsController < Groups::ApplicationController
@group.remove_avatar!
@group.save
redirect_to edit_group_path(@group)
redirect_to edit_group_path(@group), status: 302
end
end
......@@ -43,7 +43,7 @@ class Groups::HooksController < Groups::ApplicationController
def destroy
hook.destroy
redirect_to group_hooks_path(@group)
redirect_to group_hooks_path(@group), status: 302
end
private
......
......@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), notice: 'Label was removed'
redirect_to group_labels_path(@group), status: 302, notice: 'Label was removed'
end
format.js
end
......
......@@ -101,7 +101,7 @@ class GroupsController < Groups::ApplicationController
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
end
protected
......@@ -180,7 +180,7 @@ class GroupsController < Groups::ApplicationController
def build_canonical_path(group)
return group_path(group) if action_name == 'show' # root group path
params[:id] = group.to_param
url_for(params)
......
......@@ -20,25 +20,8 @@ class HealthController < ActionController::Base
render_check_results(results)
end
def metrics
results = CHECKS.flat_map(&:metrics)
response = results.map(&method(:metric_to_prom_line)).join("\n")
render text: response, content_type: 'text/plain; version=0.0.4'
end
private
def metric_to_prom_line(metric)
labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || ''
if labels.empty?
"#{metric.name} #{metric.value}"
else
"#{metric.name}{#{labels}} #{metric.value}"
end
end
def render_check_results(results)
flattened = results.flat_map do |name, result|
if result.is_a?(Gitlab::HealthChecks::Result)
......
......@@ -25,8 +25,10 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
if @authentication_result.failed? ||
(@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User))
render_unauthorized
end
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
......
class MetricsController < ActionController::Base
include RequiresHealthToken
protect_from_forgery with: :exception
before_action :validate_prometheus_metrics
def index
render text: metrics_service.metrics_text, content_type: 'text/plain; verssion=0.0.4'
end
private
def metrics_service
@metrics_service ||= MetricsService.new
end
def validate_prometheus_metrics
render_404 unless Gitlab::Metrics.prometheus_metrics_enabled?
end
end
......@@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
end
redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
redirect_to applications_profile_url,
status: 302,
notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
end
end
......@@ -5,6 +5,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
@user.save
redirect_to profile_path
redirect_to profile_path, status: 302
end
end
......@@ -39,7 +39,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
end
redirect_to profile_chat_names_path
redirect_to profile_chat_names_path, status: 302
end
private
......
......@@ -23,7 +23,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
current_user.update_secondary_emails!
respond_to do |format|
format.html { redirect_to profile_emails_url }
format.html { redirect_to profile_emails_url, status: 302 }
format.js { head :ok }
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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