Commit 7bbfbf82 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce-ee-upstream

parents 60359a71 4a2a6d52
...@@ -233,6 +233,7 @@ update-tests-metadata: ...@@ -233,6 +233,7 @@ update-tests-metadata:
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
......
...@@ -87,7 +87,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' ...@@ -87,7 +87,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 1.0'
# HAML # HAML
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
......
...@@ -125,7 +125,7 @@ GEM ...@@ -125,7 +125,7 @@ GEM
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
cause (0.1) cause (0.1)
charlock_holmes (0.7.4) charlock_holmes (0.7.3)
chronic (0.10.2) chronic (0.10.2)
chronic_duration (0.10.6) chronic_duration (0.10.6)
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
...@@ -454,9 +454,18 @@ GEM ...@@ -454,9 +454,18 @@ GEM
json-schema (2.6.2) json-schema (2.6.2)
addressable (~> 2.3.8) addressable (~> 2.3.8)
jwt (1.5.6) jwt (1.5.6)
kaminari (0.17.0) kaminari (1.0.1)
actionpack (>= 3.0.0) activesupport (>= 4.1.0)
activesupport (>= 3.0.0) kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0) knapsack (1.11.0)
rake rake
...@@ -1080,7 +1089,7 @@ DEPENDENCIES ...@@ -1080,7 +1089,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 0.17.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.11.0)
kubeclient (~> 2.2.0) kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
......
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({ $.get({
url: form.data('signatures-path'), url: form.data('signatures-path'),
data: form.serialize(), data: form.serialize(),
}).done((response) => { }).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => { response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
}); });
......
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
`,
};
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
created() {
this.illustrationSvg = illustrationSvg;
},
};
</script>
<template>
<div
v-if="!calloutDismissed"
class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i
aria-hidden="true"
class="fa fa-times">
</i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout', el: '#pipeline-schedules-callout',
......
...@@ -48,6 +48,27 @@ ...@@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`; return `${this.job.name} - ${this.job.status.label}`;
}, },
}, },
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
mounted() {
this.stopDropdownClickPropagation();
},
}; };
</script> </script>
<template> <template>
......
...@@ -29,12 +29,10 @@ export default { ...@@ -29,12 +29,10 @@ export default {
editMode() { editMode() {
if (this.editMode) { if (this.editMode) {
$('.project-refs-form').addClass('disabled'); $('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show(); $('.js-tree-ref-target-holder').show();
$('.project-refs-target-form').show();
} else { } else {
$('.project-refs-form').removeClass('disabled'); $('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide(); $('.js-tree-ref-target-holder').hide();
$('.project-refs-target-form').hide();
} }
}, },
}, },
......
...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; ...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() { mounted() {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}, },
computed: { computed: {
html() { html() {
...@@ -12,10 +12,16 @@ export default { ...@@ -12,10 +12,16 @@ export default {
}, },
}, },
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}); });
}, },
}, },
...@@ -24,9 +30,23 @@ export default { ...@@ -24,9 +30,23 @@ export default {
<template> <template>
<div> <div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div> <div
<div v-if="activeFile.render_error" class="vertical-center render-error"> v-if="!activeFile.render_error"
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div> </div>
</div> </div>
</template> </template>
...@@ -33,32 +33,30 @@ const RepoSidebar = { ...@@ -33,32 +33,30 @@ const RepoSidebar = {
}); });
}, },
linkClicked(clickedFile) { fileClicked(clickedFile) {
let url = '';
let file = clickedFile; let file = clickedFile;
if (typeof file === 'object') {
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
url = file.url; Service.url = file.url;
Service.url = url; Helper.getContent(file)
// I need to refactor this to do the `then` here. .then(() => {
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
}); })
} .catch(Helper.loadingError);
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
} }
}, },
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
...@@ -82,7 +80,7 @@ export default RepoSidebar; ...@@ -82,7 +80,7 @@ export default RepoSidebar;
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="isRoot"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/> @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file <repo-loading-file
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
...@@ -94,7 +92,7 @@ export default RepoSidebar; ...@@ -94,7 +92,7 @@ export default RepoSidebar;
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini" :is-mini="isMini"
@linkclicked="linkClicked(file)" @linkclicked="fileClicked(file)"
:is-tree="isTree" :is-tree="isTree"
:has-files="!!files.length" :has-files="!!files.length"
:active-file="activeFile"/> :active-file="activeFile"/>
......
...@@ -10,6 +10,12 @@ const RepoTab = { ...@@ -10,6 +10,12 @@ const RepoTab = {
}, },
computed: { computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times': !this.tab.changed, 'fa-times': !this.tab.changed,
...@@ -34,12 +40,24 @@ export default RepoTab; ...@@ -34,12 +40,24 @@ export default RepoTab;
<template> <template>
<li> <li>
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> <a
<i class="fa" :class="changedClass"></i> href="#0"
class="close"
@click.prevent="xClicked(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a> </a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> <a
href="#"
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li> </li>
</template> </template>
<script> <script>
import Vue from 'vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
...@@ -14,29 +13,19 @@ const RepoTabs = { ...@@ -14,29 +13,19 @@ const RepoTabs = {
data: () => Store, data: () => Store,
methods: { methods: {
isOverflow() {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) { xClicked(file) {
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
}, },
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
}; };
export default RepoTabs; export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> <ul
v-if="isMini"
id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </ul>
......
...@@ -10,7 +10,10 @@ function repoEditorLoader() { ...@@ -10,7 +10,10 @@ function repoEditorLoader() {
Store.monaco = monaco; Store.monaco = monaco;
Store.monacoLoading = false; Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, reject); }, () => {
Store.monacoLoading = false;
reject();
});
}); });
} }
......
...@@ -33,12 +33,16 @@ const RepoHelper = { ...@@ -33,12 +33,16 @@ const RepoHelper = {
? window.performance ? window.performance
: Date, : Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getBranch() { getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref'); return $('button.dropdown-menu-toggle').attr('data-ref');
}, },
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop(); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext'; return foundLang ? foundLang.id : 'plaintext';
...@@ -135,21 +139,19 @@ const RepoHelper = { ...@@ -135,21 +139,19 @@ const RepoHelper = {
return isRoot; return isRoot;
}, },
getContent(treeOrFile, cb) { getContent(treeOrFile) {
let file = treeOrFile; let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true); // const loadingData = RepoHelper.setLoading(true);
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
// RepoHelper.setLoading(false, loadingData); // RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
if (data.binary) { if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
...@@ -188,9 +190,8 @@ const RepoHelper = { ...@@ -188,9 +190,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url; newFile.url = file.url;
if (newFile.render_error === 'too_large') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = '';
...@@ -199,10 +200,6 @@ const RepoHelper = { ...@@ -199,10 +200,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) { serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message; simpleBlob.lastCommitMessage = blob.last_commit.message;
...@@ -226,7 +223,7 @@ const RepoHelper = { ...@@ -226,7 +223,7 @@ const RepoHelper = {
type, type,
name, name,
url, url,
icon: RepoHelper.toFA(icon), icon: `fa-${icon}`,
level: 0, level: 0,
loading: false, loading: false,
}; };
...@@ -244,7 +241,7 @@ const RepoHelper = { ...@@ -244,7 +241,7 @@ const RepoHelper = {
setTimeout(() => { setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = 12000; tabs.scrollLeft = tabs.scrollWidth;
}, 200); }, 200);
}, },
......
...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; ...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
$('.project-refs-target-form').hide(); $('.js-tree-ref-target-holder').hide();
$('.fa-long-arrow-right').hide();
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = { const RepoService = {
url: '', url: '',
...@@ -22,6 +23,7 @@ const RepoService = { ...@@ -22,6 +23,7 @@ const RepoService = {
getRaw(url) { getRaw(url) {
return axios.get(url, { return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
}, },
...@@ -36,7 +38,7 @@ const RepoService = { ...@@ -36,7 +38,7 @@ const RepoService = {
}, },
urlIsRichBlob(url = this.url) { urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop(); const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension); return this.richExtensionRegExp.test(extension);
}, },
......
...@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper'; ...@@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
ideEl: {},
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {}, monacoInstance: {},
service: '', service: '',
editor: '',
sidebar: '',
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
...@@ -17,19 +14,10 @@ const RepoStore = { ...@@ -17,19 +14,10 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '', blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false, submitCommitsLoading: false,
binaryLoaded: false,
dialog: { dialog: {
open: false, open: false,
title: '', title: '',
...@@ -45,9 +33,6 @@ const RepoStore = { ...@@ -45,9 +33,6 @@ const RepoStore = {
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch', targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: { binaryTypes: {
png: false, png: false,
md: false, md: false,
...@@ -58,7 +43,6 @@ const RepoStore = { ...@@ -58,7 +43,6 @@ const RepoStore = {
tree: false, tree: false,
blob: false, blob: false,
}, },
readOnly: true,
resetBinaryTypes() { resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => { Object.keys(RepoStore.binaryTypes).forEach((key) => {
...@@ -96,7 +80,6 @@ const RepoStore = { ...@@ -96,7 +80,6 @@ const RepoStore = {
if (file.binary) { if (file.binary) {
RepoStore.blobRaw = file.base64; RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
...@@ -238,4 +221,5 @@ const RepoStore = { ...@@ -238,4 +221,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview'; return RepoStore.currentBlobView === 'repo-preview';
}, },
}; };
export default RepoStore; export default RepoStore;
...@@ -290,6 +290,10 @@ ...@@ -290,6 +290,10 @@
.gpg-status-box { .gpg-status-box {
&:empty {
display: none;
}
&.valid { &.valid {
@include green-status-color; @include green-status-color;
} }
......
...@@ -453,7 +453,10 @@ ul.notes { ...@@ -453,7 +453,10 @@ ul.notes {
} }
.note-actions { .note-actions {
align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
display: inline-flex;
align-items: center;
// For PhantomJS that does not support flex // For PhantomJS that does not support flex
float: right; float: right;
margin-left: 10px; margin-left: 10px;
...@@ -463,18 +466,12 @@ ul.notes { ...@@ -463,18 +466,12 @@ ul.notes {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
} }
.more-actions { .more-actions {
display: inline-block; float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -482,16 +479,10 @@ ul.notes { ...@@ -482,16 +479,10 @@ ul.notes {
} }
.more-actions-toggle { .more-actions-toggle {
padding: 0;
&:hover .icon, &:hover .icon,
&:focus .icon { &:focus .icon {
color: $blue-600; color: $blue-600;
} }
.icon {
padding: 0 6px;
}
} }
.more-actions-dropdown { .more-actions-dropdown {
...@@ -519,28 +510,42 @@ ul.notes { ...@@ -519,28 +510,42 @@ ul.notes {
@include notes-media('max', $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
}
}
.note-action-button { .note-actions-item {
margin-left: 0; margin-left: 15px;
} display: flex;
align-items: center;
&.more-actions {
// compensate for narrow icon
margin-left: 10px;
} }
} }
.note-action-button { .note-action-button {
display: inline; line-height: 1;
line-height: 20px; padding: 0;
min-width: 16px;
color: $gray-darkest;
.fa { .fa {
color: $gray-darkest;
position: relative; position: relative;
font-size: 17px; font-size: 16px;
} }
svg { svg {
height: 16px; height: 16px;
width: 16px; width: 16px;
fill: $gray-darkest; top: 0;
vertical-align: text-top; vertical-align: text-top;
path {
fill: currentColor;
}
} }
.award-control-icon-positive, .award-control-icon-positive,
...@@ -613,10 +618,7 @@ ul.notes { ...@@ -613,10 +618,7 @@ ul.notes {
.note-role { .note-role {
position: relative; position: relative;
top: -2px; padding: 0 7px;
display: inline-block;
padding-left: 7px;
padding-right: 7px;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
margin-right: 15px; margin-right: 15px;
} }
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb { .repo-breadcrumb {
li:last-of-type { li:last-of-type {
position: relative; position: relative;
......
...@@ -6,6 +6,13 @@ module CycleAnalyticsParams ...@@ -6,6 +6,13 @@ module CycleAnalyticsParams
end end
def start_date(params) def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago case params[:start_date]
when '7'
7.days.ago
when '30'
30.days.ago
else
90.days.ago
end
end end
end end
...@@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def index def index
params[:sort] ||= 'latest_activity_desc' params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
params[:trending] = true params[:trending] = true
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def starred def starred
@projects = load_projects.reorder('star_count DESC').page(params[:page]) @projects = load_projects.reorder('star_count DESC')
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: params) ProjectsFinder.new(current_user: current_user, params: params)
.execute.includes(:route, namespace: :route) .execute
.includes(:route, namespace: :route)
.page(params[:page])
.without_count
end end
end end
...@@ -65,7 +65,7 @@ module GroupsHelper ...@@ -65,7 +65,7 @@ module GroupsHelper
end end
def remove_group_message(group) def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name } { group_name: group.name }
end end
......
module PaginationHelper
def paginate_collection(collection, remote: nil)
if collection.is_a?(Kaminari::PaginatableWithoutCount)
paginate_without_count(collection)
elsif collection.respond_to?(:total_pages)
paginate_with_count(collection, remote: remote)
end
end
def paginate_without_count(collection)
render(
'kaminari/gitlab/without_count',
previous_path: path_to_prev_page(collection),
next_path: path_to_next_page(collection)
)
end
def paginate_with_count(collection, remote: nil)
paginate(collection, remote: remote, theme: 'gitlab')
end
end
...@@ -80,7 +80,7 @@ module ProjectsHelper ...@@ -80,7 +80,7 @@ module ProjectsHelper
end end
def remove_project_message(project) def remove_project_message(project)
_("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace } { project_name_with_namespace: project.name_with_namespace }
end end
......
...@@ -11,11 +11,11 @@ module Emails ...@@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type @member_source_type = member_source_type
@member_id = member_id @member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) admins = member_source.members.owners_and_masters.pluck(:notification_email)
# A project in a group can have no explicit owners/masters, in that case # A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters. # we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group if admins.empty? && member_source.respond_to?(:group) && member_source.group
admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) admins = member_source.group.members.owners_and_masters.pluck(:notification_email)
end end
mail(to: admins, mail(to: admins,
......
...@@ -258,21 +258,39 @@ class Group < Namespace ...@@ -258,21 +258,39 @@ class Group < Namespace
end end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
users_with_parents.pluck(:id) members_with_parents.pluck(:user_id)
end end
def members_with_parents def members_with_parents
GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) # Avoids an unnecessary SELECT when the group has no parents
source_ids =
if parent_id
self_and_ancestors.reorder(nil).select(:id)
else
id
end
GroupMember
.active_without_invites
.where(source_id: source_ids)
end
def members_with_descendants
GroupMember
.active_without_invites
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end end
def users_with_parents def users_with_parents
User.where(id: members_with_parents.select(:user_id)) User
.where(id: members_with_parents.select(:user_id))
.reorder(nil)
end end
def users_with_descendants def users_with_descendants
members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) User
.where(id: members_with_descendants.select(:user_id))
User.where(id: members_with_descendants.select(:user_id)) .reorder(nil)
end end
def max_member_access_for_user(user) def max_member_access_for_user(user)
......
...@@ -42,9 +42,20 @@ class Member < ActiveRecord::Base ...@@ -42,9 +42,20 @@ class Member < ActiveRecord::Base
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_active = User.arel_table[:state].eq(:active) user_is_active = User.arel_table[:state].eq(:active)
includes(:user).references(:users) user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
.where(is_external_invite.or(user_is_active))
left_join_users
.where(user_ok)
.where(requested_at: nil)
.reorder(nil)
end
# Like active, but without invites. For when a User is required.
scope :active_without_invites, -> do
left_join_users
.where(users: { state: 'active' })
.where(requested_at: nil) .where(requested_at: nil)
.reorder(nil)
end end
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
......
...@@ -962,10 +962,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -962,10 +962,13 @@ class MergeRequest < ActiveRecord::Base
true true
end end
<<<<<<< HEAD
def base_pipeline def base_pipeline
@base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha) @base_pipeline ||= project.pipelines.find_by(sha: merge_request_diff&.base_commit_sha)
end end
=======
>>>>>>> 4a2a6d521a260981482ee8e4931ebf06cb4f5b6a
private private
def write_ref def write_ref
......
...@@ -161,6 +161,14 @@ class Namespace < ActiveRecord::Base ...@@ -161,6 +161,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors .base_and_ancestors
end end
def self_and_ancestors
return self.class.where(id: id) unless parent_id
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
end
# Returns all the descendants of the current namespace. # Returns all the descendants of the current namespace.
def descendants def descendants
Gitlab::GroupHierarchy Gitlab::GroupHierarchy
...@@ -168,6 +176,12 @@ class Namespace < ActiveRecord::Base ...@@ -168,6 +176,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants .base_and_descendants
end end
def self_and_descendants
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
[owner_id] [owner_id]
end end
......
...@@ -200,7 +200,6 @@ class Project < ActiveRecord::Base ...@@ -200,7 +200,6 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
...@@ -1396,6 +1395,10 @@ class Project < ActiveRecord::Base ...@@ -1396,6 +1395,10 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch # @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
def forks_count
Projects::ForksCountService.new(self).count
end
private private
def cross_namespace_reference?(from) def cross_namespace_reference?(from)
......
...@@ -745,9 +745,9 @@ class User < ActiveRecord::Base ...@@ -745,9 +745,9 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w[username skype linkedin twitter].each do |attr| %i[skype linkedin twitter].each do |attr|
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend value = self[attr]
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend self[attr] = Sanitize.clean(value) if value.present?
end end
end end
......
...@@ -129,6 +129,8 @@ module Projects ...@@ -129,6 +129,8 @@ module Projects
project.repository.before_delete project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete
Projects::ForksCountService.new(project).delete_cache
end end
end end
end end
...@@ -21,11 +21,17 @@ module Projects ...@@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
new_project new_project
end end
private private
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
def allowed_visibility_level def allowed_visibility_level
project_level = @project.visibility_level project_level = @project.visibility_level
......
module Projects
# Service class for getting and caching the number of forks of a project.
class ForksCountService
def initialize(project)
@project = project
end
def count
Rails.cache.fetch(cache_key) { uncached_count }
end
def refresh_cache
Rails.cache.write(cache_key, uncached_count)
end
def delete_cache
Rails.cache.delete(cache_key)
end
private
def uncached_count
@project.forks.count
end
def cache_key
['projects', @project.id, 'forks_count']
end
end
end
...@@ -13,7 +13,13 @@ module Projects ...@@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr) ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end end
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy @project.forked_project_link.destroy
end end
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
end end
end end
.gl-pagination
%ul.pagination.clearfix
- if previous_path
%li.prev
= link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
- if next_path
%li.next
= link_to(t('views.pagination.next'), next_path, rel: 'next')
- if commit.has_signature? - if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
%i.fa.fa-spinner.fa-spin
...@@ -39,6 +39,9 @@ ...@@ -39,6 +39,9 @@
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
%li %li
%a{ "href" => "#", "data-value" => "30" } %a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 30) }} {{ n__('Last %d day', 'Last %d days', 30) }}
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= icon('rss') = icon('rss')
= render 'projects/issues/export_issues/button' = render 'projects/issues/export_issues/button'
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project, = link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id), issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }), milestone_id: issues_finder.milestones.first.try(:id) }),
......
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" = button_tag "Edit merge requests", class: "btn js-bulk-update-toggle"
- if merge_project - if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request New merge request
...@@ -17,24 +17,32 @@ ...@@ -17,24 +17,32 @@
"inline-template" => true, "inline-template" => true,
"ref" => "note_#{note.id}" } "ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button", .note-actions-item
class: ("is-disabled" unless can_resolve), %button.note-action-button.line-resolve-btn{ type: "button",
":class" => "{ 'is-active': isResolved }", class: ("is-disabled" unless can_resolve),
":aria-label" => "buttonText", ":class" => "{ 'is-active': isResolved }",
"@click" => "resolve", ":aria-label" => "buttonText",
":title" => "buttonText", "@click" => "resolve",
":ref" => "'button'" } ":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do .note-actions-item
= icon('spinner spin') = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable - if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
- is_current_user = current_user == note.author - is_current_user = current_user == note.author
- if note_editable || !is_current_user - if note_editable || !is_current_user
.dropdown.more-actions .dropdown.more-actions.note-actions-item
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
= icon('ellipsis-v', class: 'icon') %span.icon
= custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- if note_editable
%li
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
%li.divider
- unless is_current_user - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo? - if show_new_repo?
= icon('long-arrow-right', title: 'to target branch') .tree-ref-target-holder.js-tree-ref-target-holder
= render 'shared/target_switcher', destination: 'tree', path: @path = icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo? - unless show_new_repo?
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg>
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
%strong= pluralize(@private_forks_count, 'private fork') %strong= pluralize(@private_forks_count, 'private fork')
%span &nbsp;you have no access to. %span &nbsp;you have no access to.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages = paginate_collection(projects, remote: remote)
- else - else
.nothing-here-block No projects found .nothing-here-block No projects found
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do .note-actions-item
= icon('spinner spin') = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
---
title: Improves performance of vue code by using vue files and moving svg out of data
function in pipeline schedule callout
merge_request:
author:
type: other
---
title: move edit comment button outside of dropdown
merge_request:
author:
---
title: Fix timeouts when creating projects in groups with many members
merge_request: 13508
author:
type: fixed
---
title: Prevents jobs dropdown from closing in pipeline graph
merge_request:
author:
type: fixed
---
title: Fix edit merge request and issues button inconsistent letter casing
merge_request:
author:
type: fixed
---
title: Cache the number of forks of a project
merge_request: 13535
author:
type: other
---
title: Use Prev/Next pagination for exploring projects
merge_request:
author:
---
title: Add a `Last 7 days` option for Cycle Analytics view
merge_request: 13443
author: Mehdi Lahmam (@mehlah)
type: added
- group: Response metrics (NGINX Ingress)
priority: 10
metrics:
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- nginx_upstream_requests_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
label: Total
unit: req / sec
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
- nginx_upstream_response_msecs_avg
weight: 1
queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
label: Average
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
required_metrics:
- nginx_upstream_responses_total
weight: 1
queries:
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- group: Response metrics (HA Proxy) - group: Response metrics (HA Proxy)
priority: 10 priority: 10
metrics: metrics:
...@@ -68,18 +98,18 @@ ...@@ -68,18 +98,18 @@
- nginx_upstream_response_msecs_avg - nginx_upstream_response_msecs_avg
weight: 1 weight: 1
queries: queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000' - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
label: Upstream label: Upstream
unit: ms unit: ms
- title: "HTTP Error Rate" - title: "HTTP Error Rate"
y_label: "Error Rate (%)" y_label: "HTTP 500 Errors / Sec"
required_metrics: required_metrics:
- nginx_responses_total - nginx_responses_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) / sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))' - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors label: HTTP Errors
unit: "%" unit: "errors / sec"
- group: System metrics (Kubernetes) - group: System metrics (Kubernetes)
priority: 5 priority: 5
metrics: metrics:
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddKodingToApplicationSettings < ActiveRecord::Migration class AddKodingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable Migration/SaferBooleanColumn
class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable Migration/SaferBooleanColumn
class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
...@@ -9,9 +9,21 @@ class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration ...@@ -9,9 +9,21 @@ class AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration
COLUMNS = %i[starts_at ends_at created_at updated_at message_html] COLUMNS = %i[starts_at ends_at created_at updated_at message_html]
def change class BroadcastMessage < ActiveRecord::Base
self.table_name = 'broadcast_messages'
end
def up
COLUMNS.each do |column| COLUMNS.each do |column|
BroadcastMessage.where(column => nil).delete_all
change_column_null :broadcast_messages, column, false change_column_null :broadcast_messages, column, false
end end
end end
def down
COLUMNS.each do |column|
change_column_null :broadcast_messages, column, true
end
end
end end
...@@ -154,3 +154,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE ...@@ -154,3 +154,28 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project" [kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/ [postgresql]: https://www.postgresql.org/
## Auto Monitoring
> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
* Response Metrics: latency, throughput, error rate
* System Metrics: CPU utilization, memory utilization
Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
![Auto Metrics](img/auto_monitoring.png)
### Configuring Auto Monitoring
If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required.
If you have installed GitLab using a different method:
1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
...@@ -607,10 +607,9 @@ exist, you should see something like: ...@@ -607,10 +607,9 @@ exist, you should see something like:
- With GitLab 9.2, all deployments to an environment are shown directly on the - With GitLab 9.2, all deployments to an environment are shown directly on the
monitoring dashboard monitoring dashboard
If you have enabled Prometheus for collecting metrics, you can monitor the performance behavior of your app If you have enabled [Prometheus for monitoring system and response metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html), you can monitor the performance behavior of your app running in each environment.
through the environments.
Once configured, GitLab will attempt to retrieve performance metrics for any Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any
environment which has had a successful deployment. If monitoring data was environment which has had a successful deployment. If monitoring data was
successfully retrieved, a Monitoring button will appear on the environment's successfully retrieved, a Monitoring button will appear on the environment's
detail page. detail page.
......
...@@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated. ...@@ -511,7 +511,24 @@ A forEach will cause side effects, it will be mutating the array being iterated.
$('span').tooltip('fixTitle'); $('span').tooltip('fixTitle');
``` ```
### The Javascript/Vue Accord
The goal of this accord is to make sure we are all on the same page.
1. When writing Vue, you may not use jQuery in your application.
1.1 If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery.
1.2 You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
1.3 If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
1.4 We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application.
1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers.
1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests.
1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions.
## SCSS ## SCSS
- [SCSS](style_guide_scss.md) - [SCSS](style_guide_scss.md)
......
...@@ -126,7 +126,7 @@ Let's Encrypt limits a single TLD to five certificate requests within a single w ...@@ -126,7 +126,7 @@ Let's Encrypt limits a single TLD to five certificate requests within a single w
## Installing GitLab using the Helm Chart ## Installing GitLab using the Helm Chart
> You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage. > You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage.
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab), you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future. Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) and [added the Helm repository](index.md#add-the-gitlab-helm-repository), you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
For example: For example:
```bash ```bash
......
...@@ -153,6 +153,14 @@ Find this option under your project's settings. ...@@ -153,6 +153,14 @@ Find this option under your project's settings.
GitLab administrators can use the admin interface to move any project to any namespace if needed. GitLab administrators can use the admin interface to move any project to any namespace if needed.
## Sharing a project with a group
You can [share your projects with a group](../project/members/share_project_with_groups.md)
and give your group members access to the project all at once.
Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
you can [lock the sharing with group feature](#share-with-group-lock-ees-eep).
## Manage group memberships via LDAP ## Manage group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
...@@ -203,7 +211,7 @@ request to add new user to project through API will not be possible. ...@@ -203,7 +211,7 @@ request to add new user to project through API will not be possible.
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/) In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing it is possible to prevent projects in a group from [sharing
a project with another group](../../workflow/share_projects_with_other_groups.md). a project with another group](../project/members/share_project_with_groups.md).
This allows for tighter control over project access. This allows for tighter control over project access.
For example, consider you have two distinct teams (Group A and Group B) For example, consider you have two distinct teams (Group A and Group B)
......
...@@ -12,8 +12,8 @@ will be unassigned automatically. ...@@ -12,8 +12,8 @@ will be unassigned automatically.
GitLab administrators receive all permissions. GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members To add or import a user, you can follow the
documentation](../workflow/add-user/add-user.md). [project members documentation](../user/project/members/index.md).
## Project ## Project
......
...@@ -98,7 +98,11 @@ from your fork to the upstream project ...@@ -98,7 +98,11 @@ from your fork to the upstream project
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data) - [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md) - [Importing and exporting projects between GitLab instances](settings/import_export.md)
## Leave a project ## Project's members
Learn how to [add members to your projects](members/index.md).
### Leave a project
**Leave project** will only display on the project's dashboard **Leave project** will only display on the project's dashboard
when a project is part of a group (under a when a project is part of a group (under a
......
...@@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/ ...@@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/
## Configuration ## Configuration
Each GitLab project can be configured to connect to a different JIRA instance. Each GitLab project can be configured to connect to a different JIRA instance. That
means one GitLab project maps to _all_ JIRA projects in that JIRA instance once
the configuration is set up. Therefore, you don't have to explicitly associate
one GitLab project to any JIRA project. Once the configuration is set up, any JIRA
projects in the JIRA instance are already mapped to the GitLab project.
If you have one JIRA instance you can pre-fill the settings page with a default If you have one JIRA instance you can pre-fill the settings page with a default
template, see the [Services Templates][services-templates] docs. template, see the [Services Templates][services-templates] docs.
...@@ -103,7 +108,6 @@ in the table below. ...@@ -103,7 +108,6 @@ in the table below.
| ----- | ----------- | | ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | | `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | | `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
......
...@@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight ...@@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments ### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments
With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>). version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>).
1. Read how to configure the bundled Prometheus server in the 1. Read how to configure the bundled Prometheus server in the
[Administration guide][gitlab-prometheus-k8s-monitor]. [Administration guide][gitlab-prometheus-k8s-monitor].
...@@ -133,6 +133,8 @@ to integrate with. ...@@ -133,6 +133,8 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment. environment which has had a successful deployment.
GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments) [Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
## Determining the performance impact of a merge ## Determining the performance impact of a merge
...@@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul ...@@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul
[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/ [prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
[prometheus-yml]:samples/prometheus.yml [prometheus-yml]:samples/prometheus.yml
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434 [gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables [ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935 [ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md [promgldocs]: ../../../administration/monitoring/prometheus/index.md
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are: GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
* [Kubernetes](kubernetes.md) * [Kubernetes](kubernetes.md)
* [NGINX](nginx.md) * [NGINX](nginx.md)
* [NGINX Ingress Controller](nginx_ingress.md)
* [HAProxy](haproxy.md) * [HAProxy](haproxy.md)
* [Amazon Cloud Watch](cloudwatch.md) * [Amazon Cloud Watch](cloudwatch.md)
...@@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will ...@@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will
GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment. GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment.
In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that, In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do that,
GitLab will look for the required metrics which have a label that GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library).
matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
For example if you are deploying to an environment named `production`, there must be a label for the metric with the value of `production`.
## Adding to the library ## Adding to the library
......
...@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro ...@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) | | Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000 | | Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m])) | | HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for NGINX metrics ## Configuring Prometheus to monitor for NGINX metrics
......
# Monitoring NGINX Ingress Controller
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress.
## Metrics supported
| Name | Query |
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
## Configuring Prometheus to monitor for NGINX ingress metrics
The easiest way to get started is to use at least version 0.9.0 of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). If you are using NGINX as your Kubernetes ingress, there is [direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release.
If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, these metrics will be automatically enabled and annotated for Prometheus monitoring.
## Specifying the Environment label
In order to isolate and only display relevant metrics for a given environment
however, GitLab needs a method to detect which labels are associated. To do this, GitLab will search metrics with appropriate labels. In this case, the `upstream` label must be of the form `<Kubernetes Namespace>-<CI_ENVIRONMENT_SLUG>-*`.
If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part.
# Project's members
You can manage the groups and users and their access levels in all of your
projects. You can also personalize the access level you give each user,
per-project.
You should have `master` or `owner` [permissions](../../permissions.md) to add
or import a new user to your project.
To view, edit, add, and remove project's members, go to your
project's **Settings > Members**.
---
## Add a user
Right next to **People**, start typing the name or username of the user you
want to add.
![Search for people](img/add_user_search_people.png)
---
Select the user and the [permission level](../../user/permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
---
Once done, hit **Add users to project** and they will be immediately added to
your project with the permissions you gave them above.
![List members](img/add_user_list_members.png)
---
From there on, you can either remove an existing user or change their access
level to the project.
## Import users from another project
You can import another project's users in your own project by hitting the
**Import members** button on the upper right corner of the **Members** menu.
In the dropdown menu, you can see only the projects you are Master on.
![Import members from another project](img/add_user_import_members_from_another_project.png)
---
Select the one you want and hit **Import project members**. A flash message
notifying you that the import was successful will appear, and the new members
are now in the project's members list. Notice that the permissions that they
had on the project you imported from are retained.
![Members list of new members](img/add_user_imported_members.png)
---
## Invite people using their e-mail address
If a user you want to give access to doesn't have an account on your GitLab
instance, you can invite them just by typing their e-mail address in the
user search field.
![Invite user by mail](img/add_user_email_search.png)
---
As you can imagine, you can mix inviting multiple people and adding existing
GitLab users to the project.
![Invite user by mail ready to submit](img/add_user_email_ready.png)
---
Once done, hit **Add users to project** and watch that there is a new member
with the e-mail address we used above. From there on, you can resend the
invitation, change their access level or even delete them.
![Invite user members list](img/add_user_email_accept.png)
---
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
## Request access to a project
As a project owner you can enable or disable non members to request access to
your project. Go to the project settings and click on **Allow users to request access**.
As a user, you can request to be a member of a project. Go to the project you'd
like to be a member of, and click the **Request Access** button on the right
side of your screen.
![Request access button](img/request_access_button.png)
---
Project owners & masters will be notified of your request and will be able to approve or
decline it on the members page.
![Manage access requests](img/access_requests_management.png)
---
If you change your mind before your request is approved, just click the
**Withdraw Access Request** button.
![Withdraw access request button](img/withdraw_access_request_button.png)
## Share project with group
Alternatively, you can [share a project with an entire group](share_project_with_groups.md) instead of adding users one by one.
# Share Projects with other Groups
You can share projects with other [groups](../../group/index.md). This makes it
possible to add a group of users to a project with a single action.
## Groups as collections of users
Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also
take advantage of the fact that groups define collections of _users_, namely the group
members.
## Sharing a project with a group of users
The primary mechanism to give a group of users, say 'Engineering', access to a project,
say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
![The 'Groups' section in the project settings screen](img/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png)
## Maximum access level
!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](img/max_access_level.png)
In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
## Share project with group lock (EES/EEP)
In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
it is possible to prevent projects in a group from [sharing
a project with another group](../members/share_project_with_groups.md).
This allows for tighter control over project access.
Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
...@@ -20,6 +20,8 @@ documentation. ...@@ -20,6 +20,8 @@ documentation.
For security reasons, when using the command line, we strongly recommend For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md). you to [connect with GitLab via SSH](../../../ssh/README.md).
## Files
## Create and edit files ## Create and edit files
Host your codebase in GitLab repositories by pushing your files to GitLab. Host your codebase in GitLab repositories by pushing your files to GitLab.
...@@ -47,6 +49,10 @@ it's easier to do so [via GitLab UI](web_editor.md): ...@@ -47,6 +49,10 @@ it's easier to do so [via GitLab UI](web_editor.md):
To get started with the command line, please read through the To get started with the command line, please read through the
[command line basics documentation](../../../gitlab-basics/command-line-commands.md). [command line basics documentation](../../../gitlab-basics/command-line-commands.md).
### Find files
Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
## Branches ## Branches
When you submit changes in a new branch, you create a new version When you submit changes in a new branch, you create a new version
......
...@@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is ...@@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is
## Project snippets ## Project snippets
Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information.
## Personal snippets ## Personal snippets
......
...@@ -19,14 +19,13 @@ ...@@ -19,14 +19,13 @@
- [Labels](../user/project/labels.md) - [Labels](../user/project/labels.md)
- (EE) [Issue weight](issue_weight.md) - (EE) [Issue weight](issue_weight.md)
- [Notification emails](notifications.md) - [Notification emails](notifications.md)
- [Project Features](project_features.md) - [Projects](../user/project/index.md)
- [Project forking workflow](forking_workflow.md) - [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md) - [Project users](../user/project/members/index.md)
- [Protected branches](../user/project/protected_branches.md) - [Protected branches](../user/project/protected_branches.md)
- [Protected tags](../user/project/protected_tags.md) - [Protected tags](../user/project/protected_tags.md)
- [Quick Actions](../user/project/quick_actions.md) - [Quick Actions](../user/project/quick_actions.md)
- [Sharing a project with a group](share_with_group.md) - [Sharing projects with groups](../user/project/members/share_project_with_groups.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Time tracking](time_tracking.md) - [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md) - [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
......
# Project users This document was moved to [../../user/project/members/index.md](../../user/project/members/index.md)
You can manage the groups and users and their access levels in all of your
projects. You can also personalize the access level you give each user,
per-project.
You should have `master` or `owner` permissions to add or import a new user
to your project.
The first step to add or import a user, go to your project and click on
**Members** in the drop-down menu on the right side of your screen.
![Members](img/add_user_members_menu.png)
---
## Add a user
Right next to **People**, start typing the name or username of the user you
want to add.
![Search for people](img/add_user_search_people.png)
---
Select the user and the [permission level](../../user/permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
---
Once done, hit **Add users to project** and they will be immediately added to
your project with the permissions you gave them above.
![List members](img/add_user_list_members.png)
---
From there on, you can either remove an existing user or change their access
level to the project.
## Import users from another project
You can import another project's users in your own project by hitting the
**Import members** button on the upper right corner of the **Members** menu.
In the dropdown menu, you can see only the projects you are Master on.
![Import members from another project](img/add_user_import_members_from_another_project.png)
---
Select the one you want and hit **Import project members**. A flash message
notifying you that the import was successful will appear, and the new members
are now in the project's members list. Notice that the permissions that they
had on the project you imported from are retained.
![Members list of new members](img/add_user_imported_members.png)
---
## Invite people using their e-mail address
If a user you want to give access to doesn't have an account on your GitLab
instance, you can invite them just by typing their e-mail address in the
user search field.
![Invite user by mail](img/add_user_email_search.png)
---
As you can imagine, you can mix inviting multiple people and adding existing
GitLab users to the project.
![Invite user by mail ready to submit](img/add_user_email_ready.png)
---
Once done, hit **Add users to project** and watch that there is a new member
with the e-mail address we used above. From there on, you can resend the
invitation, change their access level or even delete them.
![Invite user members list](img/add_user_email_accept.png)
---
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
## Request access to a project
As a project owner you can enable or disable non members to request access to
your project. Go to the project settings and click on **Allow users to request access**.
As a user, you can request to be a member of a project. Go to the project you'd
like to be a member of, and click the **Request Access** button on the right
side of your screen.
![Request access button](img/request_access_button.png)
---
Project owners & masters will be notified of your request and will be able to approve or
decline it on the members page.
![Manage access requests](img/access_requests_management.png)
---
If you change your mind before your request is approved, just click the
**Withdraw Access Request** button.
![Withdraw access request button](img/withdraw_access_request_button.png)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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