Commit 1578ee93 authored by Tim Zallmann's avatar Tim Zallmann

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

parents 278bf413 6d859009
...@@ -260,6 +260,7 @@ gem 'premailer-rails', '~> 1.9.0' ...@@ -260,6 +260,7 @@ gem 'premailer-rails', '~> 1.9.0'
# I18n # I18n
gem 'ruby_parser', '~> 3.8', require: false gem 'ruby_parser', '~> 3.8', require: false
gem 'rails-i18n', '~> 4.0.9'
gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
......
...@@ -646,6 +646,9 @@ GEM ...@@ -646,6 +646,9 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
rails-i18n (4.0.9)
i18n (~> 0.7)
railties (~> 4.0)
railties (4.2.8) railties (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
activesupport (= 4.2.8) activesupport (= 4.2.8)
...@@ -1054,6 +1057,7 @@ DEPENDENCIES ...@@ -1054,6 +1057,7 @@ DEPENDENCIES
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 4.2.8) rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 2.2)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
......
/* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import commitPipelinesTable from './pipelines_table.vue';
import CommitPipelinesTable from './pipelines_table';
Vue.use(VueResource);
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Used in:
* * - Commit details View > Pipelines Tab > Pipelines Table.
* Renders Pipelines table in pipelines tab in the commits show view. * - Merge Request details View > Pipelines Tab > Pipelines Table.
* - New Merge Request View > Pipelines Tab > Pipelines Table.
*/ */
// export for use in merge_request_tabs.js (TODO: remove this hack) const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
$(() => {
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
document.addEventListener('DOMContentLoaded', () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); const table = new CommitPipelinesTable({
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
} }
}); });
<script>
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default {
props: {
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
mixins: [
pipelinesMixin,
],
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
},
computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
},
methods: {
successCallback(resp) {
const response = resp.json();
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.setCommonData(pipelines);
},
},
};
</script>
<template>
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
/>
<error-state
v-if="shouldRenderErrorState"
/>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
</template>
...@@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar {
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar'); this.$page = $('.page-with-sidebar');
this.$sidebar = $('.right-sidebar'); this.$sidebar = $('.right-sidebar');
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
this.$bulkEditSubmitBtn = $('.update-selected-issues'); this.$bulkEditSubmitBtn = $('.update-selected-issues');
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle'); this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
...@@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar {
toggleSidebarDisplay(show) { toggleSidebarDisplay(show) {
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show);
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
} }
......
...@@ -51,6 +51,11 @@ export default { ...@@ -51,6 +51,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: { updatedAt: {
type: String, type: String,
required: false, required: false,
...@@ -105,6 +110,7 @@ export default { ...@@ -105,6 +110,7 @@ export default {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
}); });
return { return {
......
...@@ -37,18 +37,7 @@ ...@@ -37,18 +37,7 @@
}); });
}, },
taskStatus() { taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); this.updateTaskStatusText();
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
}, },
}, },
methods: { methods: {
...@@ -64,9 +53,24 @@ ...@@ -64,9 +53,24 @@
}); });
} }
}, },
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
}, },
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
this.updateTaskStatusText();
}, },
}; };
</script> </script>
......
...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
}, },
}); });
}, },
......
export default class Store { export default class Store {
constructor({ constructor(initialState) {
titleHtml, this.state = initialState;
titleText,
descriptionHtml,
descriptionText,
updatedAt,
updatedByName,
updatedByPath,
}) {
this.state = {
titleHtml,
titleText,
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt,
updatedByName,
updatedByPath,
};
this.formState = { this.formState = {
title: '', title: '',
confidential: false, confidential: false,
......
This diff is collapsed.
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
/* global Flash */ /* global Flash */
/* global notes */ /* global notes */
import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import './breakpoints'; import './breakpoints';
import './flash'; import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import commitPipelinesTable from './commit/pipelines/pipelines_table.vue';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -233,11 +235,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -233,11 +235,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
} }
mountPipelinesView() { mountPipelinesView() {
this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
// $mount(el) replaces the el with the new rendered component. We need it in order to mount // $mount(el) replaces the el with the new rendered component. We need it in order to mount
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view') pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
.appendChild(this.commitPipelinesTable.$el);
} }
loadDiff(source) { loadDiff(source) {
......
...@@ -187,7 +187,7 @@ const normalizeNewlines = function(str) { ...@@ -187,7 +187,7 @@ const normalizeNewlines = function(str) {
if ($textarea.val() !== '') { if ($textarea.val() !== '') {
return; return;
} }
myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes')); myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes'));
if (myLastNote.length) { if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit'); myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]); return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
......
<script> <script>
/* eslint-disable no-new, no-alert */ /* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default { export default {
props: { props: {
...@@ -11,53 +11,42 @@ export default { ...@@ -11,53 +11,42 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
title: { title: {
type: String, type: String,
required: true, required: true,
}, },
icon: { icon: {
type: String, type: String,
required: true, required: true,
}, },
cssClass: { cssClass: {
type: String, type: String,
required: true, required: true,
}, },
confirmActionMessage: { confirmActionMessage: {
type: String, type: String,
required: false, required: false,
}, },
}, },
components: { components: {
loadingIcon, loadingIcon,
}, },
mixins: [
tooltipMixin,
],
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
iconClass() { iconClass() {
return `fa fa-${this.icon}`; return `fa fa-${this.icon}`;
}, },
buttonClass() { buttonClass() {
return `btn has-tooltip ${this.cssClass}`; return `btn ${this.cssClass}`;
}, },
}, },
methods: { methods: {
onClick() { onClick() {
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
...@@ -66,21 +55,11 @@ export default { ...@@ -66,21 +55,11 @@ export default {
this.makeRequest(); this.makeRequest();
} }
}, },
makeRequest() { makeRequest() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
}, },
}; };
...@@ -95,10 +74,12 @@ export default { ...@@ -95,10 +74,12 @@ export default {
:aria-label="title" :aria-label="title"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
ref="tooltip"
:disabled="isLoading"> :disabled="isLoading">
<i <i
:class="iconClass" :class="iconClass"
aria-hidden="true" /> aria-hidden="true">
</i>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</button> </button>
</template> </template>
<script> <script>
import Visibility from 'visibilityjs';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import eventHub from '../event_hub'; import pipelinesMixin from '../mixins/pipelines';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import emptyState from './empty_state.vue';
import errorState from './error_state.vue';
import navigationTabs from './navigation_tabs.vue'; import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import navigationControls from './nav_controls.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Poll from '../../lib/utils/poll';
export default { export default {
props: { props: {
...@@ -20,13 +14,12 @@ ...@@ -20,13 +14,12 @@
}, },
components: { components: {
tablePagination, tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs, navigationTabs,
navigationControls, navigationControls,
loadingIcon,
}, },
mixins: [
pipelinesMixin,
],
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
...@@ -47,11 +40,6 @@ ...@@ -47,11 +40,6 @@
state: this.store.state, state: this.store.state,
apiScope: 'all', apiScope: 'all',
pagenum: 1, pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
}; };
}, },
computed: { computed: {
...@@ -62,9 +50,6 @@ ...@@ -62,9 +50,6 @@
const scope = gl.utils.getParameterByName('scope'); const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope; return scope === null ? 'all' : scope;
}, },
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * The empty state should only be rendered when the request is made to fetch all pipelines
...@@ -106,7 +91,6 @@ ...@@ -106,7 +91,6 @@
this.state.pipelines.length && this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage; this.state.pageInfo.total > this.state.pageInfo.perPage;
}, },
hasCiEnabled() { hasCiEnabled() {
return this.hasCi !== undefined; return this.hasCi !== undefined;
}, },
...@@ -129,37 +113,7 @@ ...@@ -129,37 +113,7 @@
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
}, },
methods: { methods: {
/** /**
...@@ -174,15 +128,6 @@ ...@@ -174,15 +128,6 @@
return param; return param;
}, },
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) { successCallback(resp) {
const response = { const response = {
headers: resp.headers, headers: resp.headers,
...@@ -190,33 +135,14 @@ ...@@ -190,33 +135,14 @@
}; };
this.store.storeCount(response.body.count); this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
this.setCommonData(response.body.pipelines);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
}; };
</script> </script>
<template> <template>
<div :class="cssClass"> <div :class="cssClass">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> v-if="!isLoading && !shouldRenderEmptyState">
...@@ -274,7 +200,6 @@ ...@@ -274,7 +200,6 @@
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
/> />
</div> </div>
......
...@@ -11,10 +11,6 @@ ...@@ -11,10 +11,6 @@
type: Array, type: Array,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
}, },
components: { components: {
loadingIcon, loadingIcon,
...@@ -31,17 +27,9 @@ ...@@ -31,17 +27,9 @@
$(this.$refs.tooltip).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) eventHub.$emit('postAction', endpoint);
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occured while making the request.');
});
}, },
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
......
...@@ -12,10 +12,6 @@ ...@@ -12,10 +12,6 @@
type: Array, type: Array,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -57,7 +53,6 @@ ...@@ -57,7 +53,6 @@
v-for="model in pipelines" v-for="model in pipelines"
:key="model.id" :key="model.id"
:pipeline="model" :pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
/> />
</div> </div>
......
<script> <script>
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import asyncButtonComponent from '../../pipelines/components/async_button.vue'; import asyncButtonComponent from './async_button.vue';
import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; import pipelinesActionsComponent from './pipelines_actions.vue';
import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
import ciBadge from './ci_badge_link.vue'; import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
import pipelineStage from '../../pipelines/components/stage.vue'; import pipelineStage from './stage.vue';
import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; import pipelineUrl from './pipeline_url.vue';
import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; import pipelinesTimeago from './time_ago.vue';
import commitComponent from './commit.vue'; import commitComponent from '../../vue_shared/components/commit.vue';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -20,10 +20,6 @@ export default { ...@@ -20,10 +20,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -271,7 +267,6 @@ export default { ...@@ -271,7 +267,6 @@ export default {
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions" :actions="pipeline.details.manual_actions"
:service="service"
/> />
<pipelines-artifacts-component <pipelines-artifacts-component
...@@ -282,7 +277,6 @@ export default { ...@@ -282,7 +277,6 @@ export default {
<async-button-component <async-button-component
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:service="service"
:endpoint="pipeline.retry_path" :endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
...@@ -291,7 +285,6 @@ export default { ...@@ -291,7 +285,6 @@ export default {
<async-button-component <async-button-component
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:service="service"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
......
import Vue from 'vue'; /* global Flash */
import '~/flash';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub';
import emptyState from '../../pipelines/components/empty_state.vue';
import errorState from '../../pipelines/components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
/** export default {
*
* Uses `pipelines-table-component` to render Pipelines table with an API call.
* Endpoint is provided in HTML and passed as `endpoint`.
* We need a store to store the received environemnts.
* We need a service to communicate with the server.
*
*/
export default Vue.component('pipelines-table', {
components: { components: {
pipelinesTableComponent, pipelinesTableComponent,
errorState, errorState,
emptyState, emptyState,
loadingIcon, loadingIcon,
}, },
computed: {
/** shouldRenderErrorState() {
* Accesses the DOM to provide the needed data. return this.hasError && !this.isLoading;
* Returns the necessary props to render `pipelines-table-component` component. },
* },
* @return {Object}
*/
data() { data() {
const store = new PipelineStore();
return { return {
endpoint: null,
helpPagePath: null,
store,
state: store.state,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
...@@ -50,49 +29,11 @@ export default Vue.component('pipelines-table', { ...@@ -50,49 +29,11 @@ export default Vue.component('pipelines-table', {
hasMadeRequest: false, hasMadeRequest: false,
}; };
}, },
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
},
/**
* When the component is about to be mounted, tell the service to fetch the data
*
* A request to fetch the pipelines will be made.
* In case of a successfull response we will store the data in the provided
* store, in case of a failed response we need to warn the user.
*
*/
beforeMount() { beforeMount() {
const element = document.querySelector('#commit-pipeline-table-view');
this.endpoint = element.dataset.endpoint;
this.helpPagePath = element.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint);
this.poll = new Poll({ this.poll = new Poll({
resource: this.service, resource: this.service,
method: 'getPipelines', method: 'getPipelines',
data: this.requestData ? this.requestData : undefined,
successCallback: this.successCallback, successCallback: this.successCallback,
errorCallback: this.errorCallback, errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest, notificationCallback: this.setIsMakingRequest,
...@@ -116,43 +57,36 @@ export default Vue.component('pipelines-table', { ...@@ -116,43 +57,36 @@ export default Vue.component('pipelines-table', {
}); });
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
eventHub.$on('postAction', this.postAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
eventHub.$on('postAction', this.postAction);
}, },
destroyed() { destroyed() {
this.poll.stop(); this.poll.stop();
}, },
methods: { methods: {
fetchPipelines() { fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines() this.service.getPipelines(this.requestData)
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback());
}
}, },
setCommonData(pipelines) {
successCallback(resp) {
const response = resp.json();
this.hasMadeRequest = true;
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true; this.updateGraphDropdown = true;
this.hasMadeRequest = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
...@@ -160,32 +94,10 @@ export default Vue.component('pipelines-table', { ...@@ -160,32 +94,10 @@ export default Vue.component('pipelines-table', {
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
} }
}, },
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occured while making the request.'));
}, },
},
template: ` };
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
`,
});
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
} }
.landing { .landing {
margin-bottom: $gl-padding; margin: $gl-padding auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
position: relative; position: relative;
......
...@@ -236,9 +236,6 @@ ...@@ -236,9 +236,6 @@
width: 35px; width: 35px;
background-color: $white-light; background-color: $white-light;
border: none; border: none;
position: static;
right: 0;
height: 100%;
outline: none; outline: none;
z-index: 1; z-index: 1;
......
...@@ -97,17 +97,19 @@ ...@@ -97,17 +97,19 @@
.issues-bulk-update.right-sidebar { .issues-bulk-update.right-sidebar {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
transition: right $sidebar-transition-duration; width: 0;
right: -$gutter-width; padding: 0;
transition: width $sidebar-transition-duration;
&.right-sidebar-expanded { &.right-sidebar-expanded {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
right: 0; width: $gutter-width;
} }
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
right: -$gutter-width; width: 0;
padding: 0;
.block { .block {
padding: 16px 0; padding: 16px 0;
...@@ -118,5 +120,6 @@ ...@@ -118,5 +120,6 @@
.issuable-sidebar { .issuable-sidebar {
padding: 0 3px; padding: 0 3px;
width: calc(100% + 35px);
} }
} }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
position: relative; position: relative;
.landing { .landing {
margin-top: 10px; margin-top: 0;
.inner-content { .inner-content {
white-space: normal; white-space: normal;
......
...@@ -90,8 +90,6 @@ ...@@ -90,8 +90,6 @@
} }
.explore-groups.landing { .explore-groups.landing {
margin-top: 10px;
.inner-content { .inner-content {
padding: 0; padding: 0;
......
...@@ -729,33 +729,3 @@ ...@@ -729,33 +729,3 @@
} }
} }
} }
.confidential-issue-warning {
background-color: $gl-gray;
border-radius: 3px;
padding: $gl-btn-padding $gl-padding;
margin-top: $gl-padding-top;
font-size: 14px;
color: $white-light;
.fa {
margin-right: 8px;
}
a {
color: $white-light;
text-decoration: underline;
}
&.affix {
position: static;
width: initial;
@media (min-width: $screen-sm-min) {
position: sticky;
position: -webkit-sticky;
top: 60px;
z-index: 200;
}
}
}
...@@ -103,6 +103,42 @@ ...@@ -103,6 +103,42 @@
} }
} }
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 12px;
align-items: center;
@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;
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%;
}
}
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding; padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
......
...@@ -33,7 +33,8 @@ class EventsFinder ...@@ -33,7 +33,8 @@ class EventsFinder
private private
def by_current_user_access(events) def by_current_user_access(events)
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) events.merge(ProjectsFinder.new(current_user: current_user).execute).
joins(:project)
end end
def by_action(events) def by_action(events)
......
...@@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder
private private
def init_collection def init_collection
only_owned = options.fetch(:only_owned, false) projects = if current_user
only_shared = options.fetch(:only_shared, false) collection_with_user
else
collection_without_user
end
projects = [] union(projects)
end
if current_user def collection_with_user
if group.users.include?(current_user) if group.users.include?(current_user)
projects << group.projects unless only_shared if only_shared?
projects << group.shared_projects unless only_owned [shared_projects]
elsif only_owned?
[owned_projects]
else else
unless only_shared [shared_projects, owned_projects]
projects << group.projects.visible_to_user(current_user)
projects << group.projects.public_to_user(current_user)
end end
else
unless only_owned if only_shared?
projects << group.shared_projects.visible_to_user(current_user) [shared_projects.public_or_visible_to_user(current_user)]
projects << group.shared_projects.public_to_user(current_user) elsif only_owned?
[owned_projects.public_or_visible_to_user(current_user)]
else
[
owned_projects.public_or_visible_to_user(current_user),
shared_projects.public_or_visible_to_user(current_user)
]
end end
end end
else
projects << group.projects.public_only unless only_shared
projects << group.shared_projects.public_only unless only_owned
end end
projects def collection_without_user
if only_shared?
[shared_projects.public_only]
elsif only_owned?
[owned_projects.public_only]
else
[shared_projects.public_only, owned_projects.public_only]
end
end end
def union(items) def union(items)
if items.one?
items.first
else
find_union(items, Project) find_union(items, Project)
end end
end
def only_owned?
options.fetch(:only_owned, false)
end
def only_shared?
options.fetch(:only_shared, false)
end
def owned_projects
group.projects
end
def shared_projects
group.shared_projects
end
end end
...@@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder ...@@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder
end end
def execute def execute
items = init_collection collection = init_collection
items = items.map do |item| collection = by_ids(collection)
item = by_ids(item) collection = by_personal(collection)
item = by_personal(item) collection = by_starred(collection)
item = by_starred(item) collection = by_trending(collection)
item = by_trending(item) collection = by_visibilty_level(collection)
item = by_visibilty_level(item) collection = by_tags(collection)
item = by_tags(item) collection = by_search(collection)
item = by_search(item) collection = by_archived(collection)
by_archived(item)
end sort(collection)
items = union(items)
sort(items)
end end
private private
def init_collection def init_collection
projects = [] if current_user
collection_with_user
else
collection_without_user
end
end
if params[:owned].present? def collection_with_user
projects << current_user.owned_projects if current_user if owned_projects?
current_user.owned_projects
else else
projects << current_user.authorized_projects if current_user if private_only?
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? current_user.authorized_projects
else
Project.public_or_visible_to_user(current_user)
end
end
end
# Builds a collection for an anonymous user.
def collection_without_user
if private_only? || owned_projects?
Project.none
else
Project.public_to_user
end
end
def owned_projects?
params[:owned].present?
end end
projects def private_only?
params[:non_public].present?
end end
def by_ids(items) def by_ids(items)
......
...@@ -68,7 +68,7 @@ module ApplicationHelper ...@@ -68,7 +68,7 @@ module ApplicationHelper
end end
end end
def avatar_icon(user_or_email = nil, size = nil, scale = 2) def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true)
user = user =
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user_or_email user_or_email
...@@ -77,7 +77,7 @@ module ApplicationHelper ...@@ -77,7 +77,7 @@ module ApplicationHelper
end end
if user if user
user.avatar_url(size: size) || default_avatar user.avatar_url(size: size, only_path: only_path) || default_avatar
else else
gravatar_icon(user_or_email, size, scale) gravatar_icon(user_or_email, size, scale)
end end
......
...@@ -138,8 +138,8 @@ module IssuablesHelper ...@@ -138,8 +138,8 @@ module IssuablesHelper
end end
output << "&ensp;".html_safe output << "&ensp;".html_safe
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
output output
end end
...@@ -216,7 +216,8 @@ module IssuablesHelper ...@@ -216,7 +216,8 @@ module IssuablesHelper
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description initialDescriptionText: issuable.description,
initialTaskStatus: issuable.task_status
} }
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
......
...@@ -266,20 +266,49 @@ class Project < ActiveRecord::Base ...@@ -266,20 +266,49 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
if user
authorized = user.
project_authorizations.
select(1).
where('project_authorizations.project_id = projects.id')
levels = Gitlab::VisibilityLevel.levels_for_user(user)
where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels)
else
public_to_user
end
end
# project features may be "disabled", "internal" or "enabled". If "internal", # project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where # they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user. # the feature is either enabled, or internal with permission for the user.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
return with_feature_enabled(feature) if user.try(:admin?) visible = [nil, ProjectFeature::ENABLED]
unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) if user&.admin?
return unconditional if user.nil? with_feature_enabled(feature)
elsif user
column = ProjectFeature.quoted_access_level_column(feature)
conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) authorized = user.project_authorizations.select(1).
authorized = user.authorized_projects.merge(conditional.reorder(nil)) where('project_authorizations.project_id = projects.id')
union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) with_project_feature.
where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible,
ProjectFeature::PRIVATE,
authorized)
else
with_feature_access_level(feature, visible)
end
end end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
......
...@@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base ...@@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base
"#{feature}_access_level".to_sym "#{feature}_access_level".to_sym
end end
def quoted_access_level_column(feature)
attribute = connection.quote_column_name(access_level_attribute(feature))
table = connection.quote_table_name(table_name)
"#{table}.#{attribute}"
end
end end
# Default scopes force us to unscope here since a service may need to check # Default scopes force us to unscope here since a service may need to check
......
...@@ -70,7 +70,7 @@ module ChatMessage ...@@ -70,7 +70,7 @@ module ChatMessage
end end
def branch_link def branch_link
"`[#{ref}](#{branch_url})`" "[#{ref}](#{branch_url})"
end end
def project_link def project_link
......
...@@ -61,7 +61,7 @@ module ChatMessage ...@@ -61,7 +61,7 @@ module ChatMessage
end end
def removed_branch_message def removed_branch_message
"#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
end end
def push_message def push_message
...@@ -102,7 +102,7 @@ module ChatMessage ...@@ -102,7 +102,7 @@ module ChatMessage
end end
def branch_link def branch_link
"`[#{ref}](#{branch_url})`" "[#{ref}](#{branch_url})"
end end
def project_link def project_link
......
...@@ -12,87 +12,121 @@ module Projects ...@@ -12,87 +12,121 @@ module Projects
TransferError = Class.new(StandardError) TransferError = Class.new(StandardError)
def execute(new_namespace) def execute(new_namespace)
if new_namespace.blank? @new_namespace = new_namespace
if @new_namespace.blank?
raise TransferError, 'Please select a new namespace for your project.' raise TransferError, 'Please select a new namespace for your project.'
end end
unless allowed_transfer?(current_user, project, new_namespace)
unless allowed_transfer?(current_user, project)
raise TransferError, 'Transfer failed, please contact an admin.' raise TransferError, 'Transfer failed, please contact an admin.'
end end
transfer(project, new_namespace)
transfer(project)
true
rescue Projects::TransferService::TransferError => ex rescue Projects::TransferService::TransferError => ex
project.reload project.reload
project.errors.add(:new_namespace, ex.message) project.errors.add(:new_namespace, ex.message)
false false
end end
def transfer(project, new_namespace) private
old_namespace = project.namespace
Project.transaction do def transfer(project)
old_path = project.path_with_namespace @old_path = project.path_with_namespace
old_group = project.group @old_group = project.group
new_path = File.join(new_namespace.try(:full_path) || '', project.path) @new_path = File.join(@new_namespace.try(:full_path) || '', project.path)
@old_namespace = project.namespace
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists?
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
if project.has_container_registry_tags? if project.has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry # We currently don't support renaming repository if it contains tags in container registry
raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
end end
project.expire_caches_before_rename(old_path) attempt_transfer_transaction
end
def attempt_transfer_transaction
Project.transaction do
project.expire_caches_before_rename(@old_path)
# Apply new namespace id and visibility level update_namespace_and_visibility(@new_namespace)
project.namespace = new_namespace
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
# Notifications # Notifications
project.send_move_instructions(old_path) project.send_move_instructions(@old_path)
# Move main repository # Move main repository
unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) unless move_repo_folder(@old_path, @new_path)
raise TransferError.new('Cannot move project') raise TransferError.new('Cannot move project')
end end
# Move wiki repo also if present # Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
# Move missing group labels to project # Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads # Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
# Move pages # Move pages
Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = old_path project.old_path_with_namespace = @old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) execute_system_hooks
end end
rescue Exception # rubocop:disable Lint/RescueException
refresh_permissions(old_namespace, new_namespace) rollback_side_effects
raise
true ensure
refresh_permissions
end end
def allowed_transfer?(current_user, project, namespace) def allowed_transfer?(current_user, project)
namespace && @new_namespace &&
can?(current_user, :change_namespace, project) && can?(current_user, :change_namespace, project) &&
namespace.id != project.namespace_id && @new_namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, @new_namespace)
end end
def refresh_permissions(old_namespace, new_namespace) def update_namespace_and_visibility(to_namespace)
# Apply new namespace id and visibility level
project.namespace = to_namespace
project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
end
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to # This ensures we only schedule 1 job for every user that has access to
# the namespaces. # the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations | user_ids = @old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations @new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute UserProjectAccessChangedService.new(user_ids).execute
end end
def rollback_side_effects
rollback_folder_move
update_namespace_and_visibility(@old_namespace)
end
def rollback_folder_move
move_repo_folder(@new_path, @old_path)
move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki")
end
def move_repo_folder(from_name, to_name)
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
end
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
end end
end end
...@@ -325,6 +325,10 @@ ...@@ -325,6 +325,10 @@
= f.label :prometheus_metrics_enabled do = f.label :prometheus_metrics_enabled do
= f.check_box :prometheus_metrics_enabled = f.check_box :prometheus_metrics_enabled
Enable Prometheus Metrics Enable Prometheus Metrics
- unless Gitlab::Metrics.metrics_folder_present?
.help-block
%strong.cred WARNING:
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
%fieldset %fieldset
%legend Background Jobs %legend Background Jobs
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -9,6 +9,12 @@ ...@@ -9,6 +9,12 @@
%li %li
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
- if defined?(@issue) && @issue.confidential?
%li.confidential-issue-warning
= icon('warning')
%span This is a confidential issue. Your comment will not be visible to the public.
%li.pull-right %li.pull-right
.toolbar-group .toolbar-group
= markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.dropzone .dropzone
.dropzone-previews.blob-upload-dropzone-previews .dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light %p.dz-message.light
- upload_link = link_to n_('UploadLink|click to upload'), '#', class: "markdown-selector" - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector"
- dropzone_text = _('Attach a file by drag &amp; drop or %{upload_link}') % { upload_link: upload_link } - dropzone_text = _('Attach a file by drag &amp; drop or %{upload_link}') % { upload_link: upload_link }
#{ dropzone_text.html_safe } #{ dropzone_text.html_safe }
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= label_tag 'start_branch', branch_label, class: 'control-label' = label_tag 'start_branch', branch_label, class: 'control-label'
.col-sm-10 .col-sm-10
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
= dropdown_tag(@project.default_branch, options: { title: n_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: n_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= render 'shared/new_merge_request_checkbox' = render 'shared/new_merge_request_checkbox'
......
...@@ -5,13 +5,6 @@ ...@@ -5,13 +5,6 @@
- can_update_issue = can?(current_user, :update_issue, @issue) - can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- if defined?(@issue) && @issue.confidential?
.confidential-issue-warning{ data: { spy: 'affix' } }
%span.confidential-issue-text
#{confidential_icon(@issue)} This issue is confidential.
%a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' }
What are confidential issues?
.clearfix.detail-page-header .clearfix.detail-page-header
.issuable-header .issuable-header
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
...@@ -26,6 +19,7 @@ ...@@ -26,6 +19,7 @@
= icon('angle-double-left') = icon('angle-double-left')
.issuable-meta .issuable-meta
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue") = issuable_meta(@issue, @project, "Issue")
.issuable-actions .issuable-actions
......
.dropdown.more-actions - is_current_user = current_user == note.author
- if note_editable || !is_current_user
.dropdown.more-actions
= 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') = icon('ellipsis-v', class: 'icon')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- if note_editable
%li %li
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
%li.divider %li.divider
- 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
Report as abuse Report as abuse
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
.form-group .form-group
.col-md-9 .col-md-9
= f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light'
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group .form-group
.col-md-9 .col-md-9
= f.label :ref, _('Target Branch'), class: 'label-light' = f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group .form-group
.col-md-9 .col-md-9
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
%aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" } %aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" }
.issuable-sidebar .issuable-sidebar.hidden
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block .block
.filter-item.inline.update-issues-btn.pull-left .filter-item.inline.update-issues-btn.pull-left
......
---
title: Fix an email parsing bug where brackets would be inserted in emails from some Outlook clients
merge_request: 9045
author: jneen
---
title: Rollback project repo move if there is an error in Projects::TransferService
merge_request: 11877
author:
---
title: Reinstate is_admin flag in users api when authenticated user is an admin
merge_request: 12211
author: rickettm
---
title: Fix for cut & pasted images not working
merge_request:
author:
---
title: Make confidential issues more obviously confidential
merge_request:
author:
---
title: Refactor ProjectsFinder#init_collection to produce more efficient queries for
retrieving projects
merge_request:
author:
...@@ -6,7 +6,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) ...@@ -6,7 +6,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
# set default directory for multiproces metrics gathering # set default directory for multiproces metrics gathering
ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' if ENV['RAILS_ENV'] == 'development' || ENV['RAILS_ENV'] == 'test'
ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir'
end
# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage # Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage
require 'bootsnap' require 'bootsnap'
......
...@@ -66,5 +66,14 @@ module.exports = function(config) { ...@@ -66,5 +66,14 @@ module.exports = function(config) {
karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
} }
if (process.env.DEBUG) {
karmaConfig.logLevel = config.LOG_DEBUG;
process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log';
}
if (process.env.CHROME_LOG_FILE) {
karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
}
config.set(karmaConfig); config.set(karmaConfig);
}; };
# GitLab Prometheus metrics
>**Note:**
Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For installations from source
you'll have to configure it yourself.
GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other [Prometheus] exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic.
To enable the GitLab Prometheus metrics:
1. Log into GitLab as an administrator, and go to the Admin area.
1. Click on the gear, then click on Settings.
1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics`
1. [Restart GitLab][restart] for the changes to take effect
## Collecting the metrics
Since the metrics endpoint is available on the same host and port as other traffic, it requires authentication. The token and URL to access is displayed on the [Health Check][health-check] page.
Currently the embedded Prometheus server is not automatically configured to collect metrics from this endpoint. We recommend setting up another Prometheus server, because the embedded server configuration is overwritten one every reconfigure of GitLab. In the future this will not be required.
## Metrics available
In this experimental phase, only a few metrics are available:
| Metric | Type | Description |
| ------ | ---- | ----------- |
| db_ping_timeout | Gauge | Whether or not the last database ping timed out |
| db_ping_success | Gauge | Whether or not the last database ping succeeded |
| db_ping_latency | Gauge | Round trip time of the database ping |
| redis_ping_timeout | Gauge | Whether or not the last redis ping timed out |
| redis_ping_success | Gauge | Whether or not the last redis ping succeeded |
| redis_ping_latency | Gauge | Round trip time of the redis ping |
| filesystem_access_latency | gauge | Latency in accessing a specific filesystem |
| filesystem_accessible | gauge | Whether or not a specific filesystem is accessible |
| filesystem_write_latency | gauge | Write latency of a specific filesystem |
| filesystem_writable | gauge | Whether or not the filesystem is writable |
| filesystem_read_latency | gauge | Read latency of a specific filesystem |
| filesystem_readable | gauge | Whether or not the filesystem is readable |
| user_sessions_logins | Counter | Counter of how many users have logged in |
[← Back to the main Prometheus page](index.md)
[29118]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29118
[Prometheus]: https://prometheus.io
[restart]: ../../restart_gitlab.md#omnibus-gitlab-restart
[health-check]: ../../user/admin_area/monitoring/health_check.md
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
Available since [Omnibus GitLab 8.17][1132]. For installations from source Available since [Omnibus GitLab 8.17][1132]. For installations from source
you'll have to install and configure it yourself. you'll have to install and configure it yourself.
The [GitLab monitor exporter] allows you to measure various GitLab metrics. The [GitLab monitor exporter] allows you to measure various GitLab metrics, pulled from Redis and the database.
To enable the GitLab monitor exporter: To enable the GitLab monitor exporter:
......
...@@ -110,6 +110,14 @@ To disable the monitoring of Kubernetes: ...@@ -110,6 +110,14 @@ To disable the monitoring of Kubernetes:
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect take effect
## GitLab Prometheus metrics
> Introduced as an experimental feature in GitLab 9.3.
GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic.
[➔ Read more about the GitLab Metrics.](gitlab_metrics.md)
## Prometheus exporters ## Prometheus exporters
There are a number of libraries and servers which help in exporting existing There are a number of libraries and servers which help in exporting existing
...@@ -143,7 +151,7 @@ The Postgres exporter allows you to measure various PostgreSQL metrics. ...@@ -143,7 +151,7 @@ The Postgres exporter allows you to measure various PostgreSQL metrics.
### GitLab monitor exporter ### GitLab monitor exporter
The GitLab monitor exporter allows you to measure various GitLab metrics. The GitLab monitor exporter allows you to measure various GitLab metrics, pulled from Redis and the database.
[➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md) [➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md)
......
...@@ -29,10 +29,10 @@ following locations: ...@@ -29,10 +29,10 @@ following locations:
- [Labels](labels.md) - [Labels](labels.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Open source license templates](templates/licenses.md)
- [Namespaces](namespaces.md) - [Namespaces](namespaces.md)
- [Notes](notes.md) (comments) - [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md) - [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md)
- [Pipelines](pipelines.md) - [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Triggers](pipeline_triggers.md)
- [Pipeline Schedules](pipeline_schedules.md) - [Pipeline Schedules](pipeline_schedules.md)
......
...@@ -62,6 +62,7 @@ GET /users ...@@ -62,6 +62,7 @@ GET /users
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null, "bio": null,
"location": null, "location": null,
"skype": "", "skype": "",
...@@ -94,6 +95,7 @@ GET /users ...@@ -94,6 +95,7 @@ GET /users
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
"web_url": "http://localhost:3000/jack_smith", "web_url": "http://localhost:3000/jack_smith",
"created_at": "2012-05-23T08:01:01Z", "created_at": "2012-05-23T08:01:01Z",
"is_admin": false,
"bio": null, "bio": null,
"location": null, "location": null,
"skype": "", "skype": "",
...@@ -197,6 +199,7 @@ Parameters: ...@@ -197,6 +199,7 @@ Parameters:
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
"web_url": "http://localhost:3000/john_smith", "web_url": "http://localhost:3000/john_smith",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false,
"bio": null, "bio": null,
"location": null, "location": null,
"skype": "", "skype": "",
......
...@@ -86,56 +86,31 @@ if your available memory changes. ...@@ -86,56 +86,31 @@ if your available memory changes.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
## GitLab Runner
We strongly advise against installing GitLab Runner on the same machine you plan
to install GitLab on. Depending on how you decide to configure GitLab Runner and
what tools you use to exercise your application in the CI environment, GitLab
Runner can consume significant amount of available memory.
Memory consumption calculations, that are available above, will not be valid if
you decide to run GitLab Runner and the GitLab Rails application on the same
machine.
It is also not safe to install everything on a single machine, because of the
[security reasons] - especially when you plan to use shell executor with GitLab
Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
## Unicorn Workers
It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
## Database ## Database
The server running the database should have _at least_ 5-10 GB of storage
available, though the exact requirements depend on the size of the GitLab
installation (e.g. the number of users, projects, etc).
We currently support the following databases: We currently support the following databases:
- PostgreSQL - PostgreSQL
- MySQL/MariaDB - MySQL/MariaDB
We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all We **highly recommend** the use of PostgreSQL instead of MySQL/MariaDB as not all
features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have features of GitLab may work with MySQL/MariaDB:
the right features to support nested groups in an efficient manner; see
<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information 1. MySQL support for subgroups was [dropped with GitLab 9.3][post].
about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). See [issue #30472][30472] for more information.
1. GitLab Geo does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication).
1. [Zero downtime migrations][zero] do not work with MySQL
Existing users using GitLab with MySQL/MariaDB are advised to Existing users using GitLab with MySQL/MariaDB are advised to
migrate to PostgreSQL instead. [migrate to PostgreSQL](../update/mysql_to_postgresql.md) instead.
The server running the database should have _at least_ 5-10 GB of storage [30472]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472
available, though the exact requirements depend on the size of the GitLab [zero]: ../update/README.md#upgrading-without-downtime
installation (e.g. the number of users, projects, etc). [post]: https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#dropping-support-for-subgroups-in-mysql
### PostgreSQL Requirements ### PostgreSQL Requirements
...@@ -154,6 +129,18 @@ CREATE EXTENSION pg_trgm; ...@@ -154,6 +129,18 @@ CREATE EXTENSION pg_trgm;
On some systems you may need to install an additional package (e.g. On some systems you may need to install an additional package (e.g.
`postgresql-contrib`) for this extension to become available. `postgresql-contrib`) for this extension to become available.
## Unicorn Workers
It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
## Redis and Sidekiq ## Redis and Sidekiq
Redis stores all user sessions and the background task queue. Redis stores all user sessions and the background task queue.
...@@ -172,6 +159,26 @@ default settings. ...@@ -172,6 +159,26 @@ default settings.
If you would like to disable Prometheus and it's exporters or read more information If you would like to disable Prometheus and it's exporters or read more information
about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md). about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md).
## GitLab Runner
We strongly advise against installing GitLab Runner on the same machine you plan
to install GitLab on. Depending on how you decide to configure GitLab Runner and
what tools you use to exercise your application in the CI environment, GitLab
Runner can consume significant amount of available memory.
Memory consumption calculations, that are available above, will not be valid if
you decide to run GitLab Runner and the GitLab Rails application on the same
machine.
It is also not safe to install everything on a single machine, because of the
[security reasons] - especially when you plan to use shell executor with GitLab
Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
## Supported web browsers ## Supported web browsers
We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11). We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
......
...@@ -11,22 +11,6 @@ There are currently 3 official ways to install GitLab: ...@@ -11,22 +11,6 @@ There are currently 3 official ways to install GitLab:
Based on your installation, choose a section below that fits your needs. Based on your installation, choose a section below that fits your needs.
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Omnibus Packages](#omnibus-packages)
- [Installation from source](#installation-from-source)
- [Installation using Docker](#installation-using-docker)
- [Upgrading between editions](#upgrading-between-editions)
- [Community to Enterprise Edition](#community-to-enterprise-edition)
- [Enterprise to Community Edition](#enterprise-to-community-edition)
- [Miscellaneous](#miscellaneous)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Omnibus Packages ## Omnibus Packages
- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) - The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html)
......
# Subgroups # Subgroups
> [Introduced][ce-2772] in GitLab 9.0. >**Notes:**
- [Introduced][ce-2772] in GitLab 9.0.
- Not available when using MySQL as external database (support removed in
GitLab 9.3 [due to performance reasons][issue]).
With subgroups (aka nested groups or hierarchical groups) you can have With subgroups (aka nested groups or hierarchical groups) you can have
up to 20 levels of nested groups, which among other things can help you to: up to 20 levels of nested groups, which among other things can help you to:
...@@ -173,3 +176,4 @@ Here's a list of what you can't do with subgroups: ...@@ -173,3 +176,4 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group [permissions]: ../../permissions.md#group
[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb [reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb
[issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600
...@@ -43,8 +43,9 @@ next to the issues that are marked as confidential. ...@@ -43,8 +43,9 @@ next to the issues that are marked as confidential.
--- ---
While inside the issue, you can see a persistent dark banner at the top of the Likewise, while inside the issue, you can see the eye-slash icon right next to
screen. the issue number, but there is also an indicator in the comment area that the
issue you are commenting on is confidential.
![Confidential issue page](img/confidential_issues_issue_page.png) ![Confidential issue page](img/confidential_issues_issue_page.png)
......
...@@ -43,11 +43,14 @@ module API ...@@ -43,11 +43,14 @@ module API
expose :external expose :external
end end
class UserWithPrivateDetails < UserPublic class UserWithAdmin < UserPublic
expose :private_token
expose :admin?, as: :is_admin expose :admin?, as: :is_admin
end end
class UserWithPrivateDetails < UserWithAdmin
expose :private_token
end
class Email < Grape::Entity class Email < Grape::Entity
expose :id, :email expose :id, :email
end end
......
...@@ -71,11 +71,16 @@ module API ...@@ -71,11 +71,16 @@ module API
end end
# #
# Discover user by ssh key # Discover user by ssh key or user id
# #
get "/discover" do get "/discover" do
if params[:key_id]
key = Key.find(params[:key_id]) key = Key.find(params[:key_id])
present key.user, with: Entities::UserSafe user = key.user
elsif params[:user_id]
user = User.find_by(id: params[:user_id])
end
present user, with: Entities::UserSafe
end end
get "/check" do get "/check" do
......
...@@ -59,7 +59,7 @@ module API ...@@ -59,7 +59,7 @@ module API
users = UsersFinder.new(current_user, params).execute users = UsersFinder.new(current_user, params).execute
entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic
present paginate(users), with: entity present paginate(users), with: entity
end end
......
...@@ -17,6 +17,13 @@ module Gitlab ...@@ -17,6 +17,13 @@ module Gitlab
def filter_replies! def filter_replies!
document.xpath('//blockquote').each(&:remove) document.xpath('//blockquote').each(&:remove)
document.xpath('//table').each(&:remove) document.xpath('//table').each(&:remove)
# bogus links with no href are sometimes added by outlook,
# and can result in Html2Text adding extra square brackets
# to the text, so we unwrap them here.
document.xpath('//a[not(@href)]').each do |link|
link.replace(link.children)
end
end end
def filtered_html def filtered_html
......
...@@ -5,8 +5,16 @@ module Gitlab ...@@ -5,8 +5,16 @@ module Gitlab
module Prometheus module Prometheus
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
def metrics_folder_present?
ENV.has_key?('prometheus_multiproc_dir') &&
::Dir.exist?(ENV['prometheus_multiproc_dir']) &&
::File.writable?(ENV['prometheus_multiproc_dir'])
end
def prometheus_metrics_enabled? def prometheus_metrics_enabled?
@prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled)
@prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized
end end
def registry def registry
...@@ -36,6 +44,12 @@ module Gitlab ...@@ -36,6 +44,12 @@ module Gitlab
NullMetric.new NullMetric.new
end end
end end
private
def prometheus_metrics_enabled_unmemoized
metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false
end
end end
end end
end end
...@@ -13,18 +13,8 @@ module Gitlab ...@@ -13,18 +13,8 @@ module Gitlab
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
scope :public_to_user, -> (user) do scope :public_to_user, -> (user = nil) do
if user where(visibility_level: VisibilityLevel.levels_for_user(user))
if user.admin?
all
elsif !user.external?
public_and_internal_only
else
public_only
end
else
public_only
end
end end
end end
...@@ -35,6 +25,18 @@ module Gitlab ...@@ -35,6 +25,18 @@ module Gitlab
class << self class << self
delegate :values, to: :options delegate :values, to: :options
def levels_for_user(user = nil)
return [PUBLIC] unless user
if user.admin?
[PRIVATE, INTERNAL, PUBLIC]
elsif user.external?
[PUBLIC]
else
[INTERNAL, PUBLIC]
end
end
def string_values def string_values
string_options.keys string_options.keys
end end
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2017-06-15 21:59-0500\n" "PO-Revision-Date: 2017-06-19 15:22-0500\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Language: es\n" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
...@@ -61,6 +61,12 @@ msgstr[1] "Ramas" ...@@ -61,6 +61,12 @@ msgstr[1] "Ramas"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}" msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Buscar ramas"
msgid "BranchSwitcherTitle|Switch branch"
msgstr "Cambiar rama"
msgid "Branches" msgid "Branches"
msgstr "Ramas" msgstr "Ramas"
...@@ -945,6 +951,9 @@ msgstr "Subir nuevo archivo" ...@@ -945,6 +951,9 @@ msgstr "Subir nuevo archivo"
msgid "Upload file" msgid "Upload file"
msgstr "Subir archivo" msgstr "Subir archivo"
msgid "UploadLink|click to upload"
msgstr "Hacer clic para subir"
msgid "Use your global notification setting" msgid "Use your global notification setting"
msgstr "Utiliza tu configuración de notificación global" msgstr "Utiliza tu configuración de notificación global"
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-15 21:59-0500\n" "POT-Creation-Date: 2017-06-19 15:13-0500\n"
"PO-Revision-Date: 2017-06-15 21:59-0500\n" "PO-Revision-Date: 2017-06-19 15:13-0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -62,6 +62,12 @@ msgstr[1] "" ...@@ -62,6 +62,12 @@ msgstr[1] ""
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "" msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
msgid "BranchSwitcherTitle|Switch branch"
msgstr ""
msgid "Branches" msgid "Branches"
msgstr "" msgstr ""
...@@ -946,6 +952,9 @@ msgstr "" ...@@ -946,6 +952,9 @@ msgstr ""
msgid "Upload file" msgid "Upload file"
msgstr "" msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
msgid "Use your global notification setting" msgid "Use your global notification setting"
msgstr "" msgstr ""
......
...@@ -16,6 +16,21 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -16,6 +16,21 @@ feature 'Issues > Labels bulk assignment', feature: true do
gitlab_sign_in user gitlab_sign_in user
end end
context 'sidebar' do
before do
enable_bulk_update
end
it 'is present when bulk edit is enabled' do
expect(page).to have_css('.issuable-sidebar')
end
it 'is not present when bulk edit is disabled' do
disable_bulk_update
expect(page).not_to have_css('.issuable-sidebar')
end
end
context 'can bulk assign' do context 'can bulk assign' do
before do before do
enable_bulk_update enable_bulk_update
...@@ -398,4 +413,8 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -398,4 +413,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
click_button 'Edit Issues' click_button 'Edit Issues'
end end
def disable_bulk_update
click_button 'Cancel'
end
end end
...@@ -19,15 +19,4 @@ describe 'Reportable note on snippets', :feature, :js do ...@@ -19,15 +19,4 @@ describe 'Reportable note on snippets', :feature, :js do
it_behaves_like 'reportable note' it_behaves_like 'reportable note'
end end
describe 'on personal snippet' do
let(:snippet) { create(:personal_snippet, :public, author: user) }
let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) }
before do
visit snippet_path(snippet)
end
it_behaves_like 'reportable note'
end
end end
MIME-Version: 1.0
Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT)
X-Originating-IP: [117.207.85.84]
In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
References: <topic/35@discourse.techapj.com>
<5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
Date: Wed, 8 Oct 2014 10:47:17 +0530
Delivered-To: arpit@techapj.com
Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com>
Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse!
From: Arpit Jalan <arpit@techapj.com>
To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US
Content-Language: en-US
X-MS-Has-Attach:
X-MS-TNEF-Correlator:
x-originating-ip: [134.68.31.227]
Content-Type: multipart/alternative;
boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_"
MIME-Version: 1.0
--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_
Content-Type: text/html; charset="utf-8"
<a name="_MailEndCompose">no brackets!</a>
--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_--
...@@ -82,42 +82,71 @@ describe ApplicationHelper do ...@@ -82,42 +82,71 @@ describe ApplicationHelper do
end end
describe 'avatar_icon' do describe 'avatar_icon' do
it 'returns an url for the avatar' do let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
user = create(:user, avatar: File.open(uploaded_image_temp_path))
avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif" context 'using an email' do
context 'when there is a matching user' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user.email).to_s).
to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) context 'when an asset_host is set in the config' do
let(:asset_host) { 'http://assets' }
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) before do
avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif" allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
end
expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) it 'returns an absolute URL on that asset host' do
expect(helper.avatar_icon(user.email, only_path: false).to_s).
to eq("#{asset_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end
context 'when only_path is set to false' do
it 'returns an absolute URL for the avatar' do
expect(helper.avatar_icon(user.email, only_path: false).to_s).
to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end end
it 'returns an url for the avatar with relative url' do context 'when the GitLab instance is at a relative URL' do
before do
stub_config_setting(relative_url_root: '/gitlab') stub_config_setting(relative_url_root: '/gitlab')
# Must be stubbed after the stub above, and separately # Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url)) stub_config_setting(url: Settings.send(:build_gitlab_url))
end
user = create(:user, avatar: File.open(uploaded_image_temp_path)) it 'returns a relative URL with the correct prefix' do
expect(helper.avatar_icon(user.email).to_s). expect(helper.avatar_icon(user.email).to_s).
to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") to eq("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end
end end
it 'calls gravatar_icon when no User exists with the given email' do context 'when no user exists for the email' do
it 'calls gravatar_icon' do
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
helper.avatar_icon('foo@example.com', 20, 2) helper.avatar_icon('foo@example.com', 20, 2)
end end
end
end
describe 'using a User' do describe 'using a user' do
it 'returns an URL for the avatar' do context 'when only_path is true' do
user = create(:user, avatar: File.open(uploaded_image_temp_path)) it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user, only_path: true).to_s).
to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end
expect(helper.avatar_icon(user).to_s). context 'when only_path is false' do
to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") it 'returns an absolute URL for the avatar' do
expect(helper.avatar_icon(user, only_path: false).to_s).
to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
end
end end
end end
end end
......
import Vue from 'vue'; import Vue from 'vue';
import PipelinesTable from '~/commit/pipelines/pipelines_table'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
describe('Pipelines table in Commits and Merge requests', () => { describe('Pipelines table in Commits and Merge requests', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
let pipeline; let pipeline;
let PipelinesTable;
preloadFixtures('static/pipelines_table.html.raw');
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
beforeEach(() => { beforeEach(() => {
loadFixtures('static/pipelines_table.html.raw'); PipelinesTable = Vue.extend(pipelinesTable);
const pipelines = getJSONFixture(jsonFixtureName).pipelines; const pipelines = getJSONFixture(jsonFixtureName).pipelines;
pipeline = pipelines.find(p => p.id === 1); pipeline = pipelines.find(p => p.id === 1);
}); });
...@@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
Vue.http.interceptors.push(pipelinesEmptyResponse); Vue.http.interceptors.push(pipelinesEmptyResponse);
this.component = new PipelinesTable({ this.component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'), propsData: {
}); endpoint: 'endpoint',
helpPagePath: 'foo',
},
}).$mount();
}); });
afterEach(function () { afterEach(function () {
...@@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
Vue.http.interceptors.push(pipelinesResponse); Vue.http.interceptors.push(pipelinesResponse);
this.component = new PipelinesTable({ this.component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'), propsData: {
}); endpoint: 'endpoint',
helpPagePath: 'foo',
},
}).$mount();
}); });
afterEach(() => { afterEach(() => {
...@@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
Vue.http.interceptors.push(pipelinesErrorResponse); Vue.http.interceptors.push(pipelinesErrorResponse);
this.component = new PipelinesTable({ this.component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'), propsData: {
}); endpoint: 'endpoint',
helpPagePath: 'foo',
},
}).$mount();
}); });
afterEach(function () { afterEach(function () {
......
...@@ -55,13 +55,20 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont ...@@ -55,13 +55,20 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merge_request) render_merge_request(example.description, merge_request)
end end
it 'merge_requests/changes_tab_with_comments.json' do |example|
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
render_merge_request(example.description, merge_request, action: :diffs, format: :json)
end
private private
def render_merge_request(fixture_file_name, merge_request) def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html)
get :show, get action,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project, project_id: project,
id: merge_request.to_param id: merge_request.to_param,
format: format
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, fixture_file_name) store_frontend_fixture(response, fixture_file_name)
......
#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } }
...@@ -95,6 +95,18 @@ describe('Description component', () => { ...@@ -95,6 +95,18 @@ describe('Description component', () => {
done(); done();
}); });
}); });
it('clears task status text when no tasks are present', (done) => {
vm.taskStatus = '0 of 0';
setTimeout(() => {
expect(
document.querySelector('.issuable-meta #task_status').textContent.trim(),
).toBe('');
done();
});
});
}); });
it('applies syntax highlighting and math when description changed', (done) => { it('applies syntax highlighting and math when description changed', (done) => {
......
...@@ -7,16 +7,20 @@ import '~/render_gfm'; ...@@ -7,16 +7,20 @@ import '~/render_gfm';
import '~/render_math'; import '~/render_math';
import '~/notes'; import '~/notes';
const upArrowKeyCode = 38;
describe('Merge request notes', () => { describe('Merge request notes', () => {
window.gon = window.gon || {}; window.gon = window.gon || {};
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.utils = gl.utils || {}; gl.utils = gl.utils || {};
const fixture = 'merge_requests/diff_comment.html.raw'; const discussionTabFixture = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixture); const changesTabJsonFixture = 'merge_requests/changes_tab_with_comments.json';
preloadFixtures(discussionTabFixture, changesTabJsonFixture);
describe('Discussion tab with diff comments', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures(fixture); loadFixtures(discussionTabFixture);
gl.utils.disableButtonIfEmptyField = _.noop; gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads'; window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:merge_requests:show'); $('body').data('page', 'projects:merge_requests:show');
...@@ -28,7 +32,7 @@ describe('Merge request notes', () => { ...@@ -28,7 +32,7 @@ describe('Merge request notes', () => {
describe('up arrow', () => { describe('up arrow', () => {
it('edits last comment when triggered in main form', () => { it('edits last comment when triggered in main form', () => {
const upArrowEvent = $.Event('keydown'); const upArrowEvent = $.Event('keydown');
upArrowEvent.which = 38; upArrowEvent.which = upArrowKeyCode;
spyOnEvent('.note:last .js-note-edit', 'click'); spyOnEvent('.note:last .js-note-edit', 'click');
...@@ -39,7 +43,7 @@ describe('Merge request notes', () => { ...@@ -39,7 +43,7 @@ describe('Merge request notes', () => {
it('edits last comment in discussion when triggered in discussion form', (done) => { it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown'); const upArrowEvent = $.Event('keydown');
upArrowEvent.which = 38; upArrowEvent.which = upArrowKeyCode;
spyOnEvent('.note-discussion .js-note-edit', 'click'); spyOnEvent('.note-discussion .js-note-edit', 'click');
...@@ -58,4 +62,38 @@ describe('Merge request notes', () => { ...@@ -58,4 +62,38 @@ describe('Merge request notes', () => {
}); });
}); });
}); });
});
describe('Changes tab with diff comments', () => {
beforeEach(() => {
const diffsResponse = getJSONFixture(changesTabJsonFixture);
const noteFormHtml = `<form class="js-new-note-form">
<textarea class="js-note-text"></textarea>
</form>`;
setFixtures(diffsResponse.html + noteFormHtml);
$('body').data('page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
describe('up arrow', () => {
it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown');
upArrowEvent.which = upArrowKeyCode;
spyOnEvent('.note:last .js-note-edit', 'click');
$('.js-discussion-reply-button').trigger('click');
setTimeout(() => {
$('.js-note-text').trigger(upArrowEvent);
expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
done();
});
});
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import asyncButtonComp from '~/pipelines/components/async_button.vue'; import asyncButtonComp from '~/pipelines/components/async_button.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipelines Async Button', () => { describe('Pipelines Async Button', () => {
let component; let component;
let spy;
let AsyncButtonComponent; let AsyncButtonComponent;
beforeEach(() => { beforeEach(() => {
AsyncButtonComponent = Vue.extend(asyncButtonComp); AsyncButtonComponent = Vue.extend(asyncButtonComp);
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
component = new AsyncButtonComponent({ component = new AsyncButtonComponent({
propsData: { propsData: {
endpoint: '/foo', endpoint: '/foo',
title: 'Foo', title: 'Foo',
icon: 'fa fa-foo', icon: 'fa fa-foo',
cssClass: 'bar', cssClass: 'bar',
service: {
postAction: spy,
},
}, },
}).$mount(); }).$mount();
}); });
...@@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => { ...@@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => {
}); });
it('should render the provided title', () => { it('should render the provided title', () => {
expect(component.$el.getAttribute('title')).toContain('Foo'); expect(component.$el.getAttribute('data-original-title')).toContain('Foo');
expect(component.$el.getAttribute('aria-label')).toContain('Foo'); expect(component.$el.getAttribute('aria-label')).toContain('Foo');
}); });
...@@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => { ...@@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => {
expect(component.$el.getAttribute('class')).toContain('bar'); expect(component.$el.getAttribute('class')).toContain('bar');
}); });
it('should call the service when it is clicked with the provided endpoint', () => {
component.$el.click();
expect(spy).toHaveBeenCalledWith('/foo');
});
it('should hide loading if request fails', () => {
spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
component = new AsyncButtonComponent({
propsData: {
endpoint: '/foo',
title: 'Foo',
icon: 'fa fa-foo',
cssClass: 'bar',
dataAttributes: {
'data-foo': 'foo',
},
service: {
postAction: spy,
},
},
}).$mount();
component.$el.click();
expect(component.$el.querySelector('.fa-spinner')).toBe(null);
});
describe('With confirm dialog', () => { describe('With confirm dialog', () => {
it('should call the service when confimation is positive', () => { it('should call the service when confimation is positive', () => {
spyOn(window, 'confirm').and.returnValue(true); spyOn(window, 'confirm').and.returnValue(true);
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); eventHub.$on('postAction', (endpoint) => {
expect(endpoint).toEqual('/foo');
});
component = new AsyncButtonComponent({ component = new AsyncButtonComponent({
propsData: { propsData: {
...@@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => { ...@@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => {
title: 'Foo', title: 'Foo',
icon: 'fa fa-foo', icon: 'fa fa-foo',
cssClass: 'bar', cssClass: 'bar',
service: {
postAction: spy,
},
confirmActionMessage: 'bar', confirmActionMessage: 'bar',
}, },
}).$mount(); }).$mount();
component.$el.click(); component.$el.click();
expect(spy).toHaveBeenCalledWith('/foo');
}); });
}); });
}); });
...@@ -3,7 +3,6 @@ import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue'; ...@@ -3,7 +3,6 @@ import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue';
describe('Pipelines Actions dropdown', () => { describe('Pipelines Actions dropdown', () => {
let component; let component;
let spy;
let actions; let actions;
let ActionsComponent; let ActionsComponent;
...@@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => { ...@@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => {
}, },
]; ];
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
component = new ActionsComponent({ component = new ActionsComponent({
propsData: { propsData: {
actions, actions,
service: {
postAction: spy,
},
}, },
}).$mount(); }).$mount();
}); });
...@@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => { ...@@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => {
).toEqual(actions.length); ).toEqual(actions.length);
}); });
it('should call the service when an action is clicked', () => {
component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
component.$el.querySelector('.js-pipeline-action-link').click();
expect(spy).toHaveBeenCalledWith(actions[0].path);
});
it('should hide loading if request fails', () => {
spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
component = new ActionsComponent({
propsData: {
actions,
service: {
postAction: spy,
},
},
}).$mount();
component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
component.$el.querySelector('.js-pipeline-action-link').click();
expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
});
it('should render a disabled action when it\'s not playable', () => { it('should render a disabled action when it\'s not playable', () => {
expect( expect(
component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
......
import Vue from 'vue'; import Vue from 'vue';
import tableRowComp from '~/vue_shared/components/pipelines_table_row.vue'; import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
describe('Pipelines Table Row', () => { describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
......
import Vue from 'vue'; import Vue from 'vue';
import pipelinesTableComp from '~/vue_shared/components/pipelines_table.vue'; import pipelinesTableComp from '~/pipelines/components/pipelines_table.vue';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
describe('Pipelines Table', () => { describe('Pipelines Table', () => {
...@@ -22,7 +22,6 @@ describe('Pipelines Table', () => { ...@@ -22,7 +22,6 @@ describe('Pipelines Table', () => {
component = new PipelinesTableComponent({ component = new PipelinesTableComponent({
propsData: { propsData: {
pipelines: [], pipelines: [],
service: {},
}, },
}).$mount(); }).$mount();
}); });
...@@ -48,7 +47,6 @@ describe('Pipelines Table', () => { ...@@ -48,7 +47,6 @@ describe('Pipelines Table', () => {
const component = new PipelinesTableComponent({ const component = new PipelinesTableComponent({
propsData: { propsData: {
pipelines: [], pipelines: [],
service: {},
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
...@@ -58,10 +56,8 @@ describe('Pipelines Table', () => { ...@@ -58,10 +56,8 @@ describe('Pipelines Table', () => {
describe('with data', () => { describe('with data', () => {
it('should render rows', () => { it('should render rows', () => {
const component = new PipelinesTableComponent({ const component = new PipelinesTableComponent({
el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
pipelines: [pipeline], pipelines: [pipeline],
service: {},
}, },
}).$mount(); }).$mount();
......
...@@ -208,5 +208,9 @@ describe Gitlab::Email::ReplyParser, lib: true do ...@@ -208,5 +208,9 @@ describe Gitlab::Email::ReplyParser, lib: true do
it "properly renders html-only email from MS Outlook" do it "properly renders html-only email from MS Outlook" do
expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010") expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010")
end end
it "does not wrap links with no href in unnecessary brackets" do
expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!")
end
end end
end end
...@@ -15,6 +15,36 @@ describe Gitlab::Metrics do ...@@ -15,6 +15,36 @@ describe Gitlab::Metrics do
end end
end end
describe '.prometheus_metrics_enabled_unmemoized' do
subject { described_class.send(:prometheus_metrics_enabled_unmemoized) }
context 'prometheus metrics enabled in config' do
before do
allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true)
end
context 'when metrics folder is present' do
before do
allow(described_class).to receive(:metrics_folder_present?).and_return(true)
end
it 'metrics are enabled' do
expect(subject).to eq(true)
end
end
context 'when metrics folder is missing' do
before do
allow(described_class).to receive(:metrics_folder_present?).and_return(false)
end
it 'metrics are disabled' do
expect(subject).to eq(false)
end
end
end
end
describe '.prometheus_metrics_enabled?' do describe '.prometheus_metrics_enabled?' do
it 'returns a boolean' do it 'returns a boolean' do
expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) expect(described_class.prometheus_metrics_enabled?).to be_in([true, false])
......
...@@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do ...@@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do
expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
end end
end end
describe '.levels_for_user' do
it 'returns all levels for an admin' do
user = double(:user, admin?: true)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns INTERNAL and PUBLIC for internal users' do
user = double(:user, admin?: false, external?: false)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns PUBLIC for external users' do
user = double(:user, admin?: false, external?: true)
expect(described_class.levels_for_user(user)).
to eq([Gitlab::VisibilityLevel::PUBLIC])
end
it 'returns PUBLIC when no user is given' do
expect(described_class.levels_for_user).
to eq([Gitlab::VisibilityLevel::PUBLIC])
end
end
end end
...@@ -4,6 +4,18 @@ describe ProjectFeature do ...@@ -4,6 +4,18 @@ describe ProjectFeature do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
describe '.quoted_access_level_column' do
it 'returns the table name and quoted column name for a feature' do
expected = if Gitlab::Database.postgresql?
'"project_features"."issues_access_level"'
else
'`project_features`.`issues_access_level`'
end
expect(described_class.quoted_access_level_column(:issues)).to eq(expected)
end
end
describe '#feature_available?' do describe '#feature_available?' do
let(:features) { %w(issues wiki builds merge_requests snippets repository) } let(:features) { %w(issues wiki builds merge_requests snippets repository) }
......
...@@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do ...@@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do
def build_message(status_text = status, name = user[:name]) def build_message(status_text = status, name = user[:name])
"<http://example.gitlab.com|project_name>:" \ "<http://example.gitlab.com|project_name>:" \
" Pipeline <http://example.gitlab.com/pipelines/123|#123>" \ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
" of branch `<http://example.gitlab.com/commits/develop|develop>`" \ " of branch <http://example.gitlab.com/commits/develop|develop>" \
" by #{name} #{status_text} in 02:00:10" " by #{name} #{status_text} in 02:00:10"
end end
end end
...@@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do ...@@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message) expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed', title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed',
subtitle: 'in [project_name](http://example.gitlab.com)', subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10', text: 'in 02:00:10',
image: '' image: ''
...@@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do ...@@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message) expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed', title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed',
subtitle: 'in [project_name](http://example.gitlab.com)', subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10', text: 'in 02:00:10',
image: '' image: ''
...@@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do ...@@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do
expect(subject.pretext).to be_empty expect(subject.pretext).to be_empty
expect(subject.attachments).to eq(message) expect(subject.attachments).to eq(message)
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed', title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by API failed',
subtitle: 'in [project_name](http://example.gitlab.com)', subtitle: 'in [project_name](http://example.gitlab.com)',
text: 'in 02:00:10', text: 'in 02:00:10',
image: '' image: ''
...@@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do ...@@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do
def build_markdown_message(status_text = status, name = user[:name]) def build_markdown_message(status_text = status, name = user[:name])
"[project_name](http://example.gitlab.com):" \ "[project_name](http://example.gitlab.com):" \
" Pipeline [#123](http://example.gitlab.com/pipelines/123)" \ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of branch `[develop](http://example.gitlab.com/commits/develop)`" \ " of branch [develop](http://example.gitlab.com/commits/develop)" \
" by #{name} #{status_text} in 02:00:10" " by #{name} #{status_text} in 02:00:10"
end end
end end
......
...@@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do context 'without markdown' do
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed to branch `<http://url.com/commits/master|master>` of '\ 'test.user pushed to branch <http://url.com/commits/master|master> of '\
'<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)') '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
expect(subject.attachments).to eq([{ expect(subject.attachments).to eq([{
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\ text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
...@@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
expect(subject.attachments).to eq( expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2") "[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
expect(subject.activity).to eq({ expect(subject.activity).to eq({
...@@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do context 'without markdown' do
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \ expect(subject.pretext).to eq('test.user pushed new tag ' \
'`<http://url.com/commits/new_tag|new_tag>` to ' \ '<http://url.com/commits/new_tag|new_tag> to ' \
'<http://url.com|project_name>') '<http://url.com|project_name>')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
...@@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding pushes' do it 'returns a message regarding pushes' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)') 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'test.user created tag', title: 'test.user created tag',
...@@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do context 'without markdown' do
it 'returns a message regarding a new branch' do it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed new branch `<http://url.com/commits/master|master>` to '\ 'test.user pushed new branch <http://url.com/commits/master|master> to '\
'<http://url.com|project_name>') '<http://url.com|project_name>')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
...@@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a new branch' do it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)') 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'test.user created branch', title: 'test.user created branch',
...@@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do
context 'without markdown' do context 'without markdown' do
it 'returns a message regarding a removed branch' do it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user removed branch `master` from <http://url.com|project_name>') 'test.user removed branch master from <http://url.com|project_name>')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
...@@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do ...@@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do
it 'returns a message regarding a removed branch' do it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'test.user removed branch `master` from [project_name](http://url.com)') 'test.user removed branch master from [project_name](http://url.com)')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
expect(subject.activity).to eq({ expect(subject.activity).to eq({
title: 'test.user removed branch', title: 'test.user removed branch',
......
...@@ -2060,4 +2060,36 @@ describe Project, models: true do ...@@ -2060,4 +2060,36 @@ describe Project, models: true do
expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i) expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i)
end end
end end
describe '.public_or_visible_to_user' do
let!(:user) { create(:user) }
let!(:private_project) do
create(:empty_project, :private, creator: user, namespace: user.namespace)
end
let!(:public_project) { create(:empty_project, :public) }
context 'with a user' do
let(:projects) do
Project.all.public_or_visible_to_user(user)
end
it 'includes projects the user has access to' do
expect(projects).to include(private_project)
end
it 'includes projects the user can see' do
expect(projects).to include(public_project)
end
end
context 'without a user' do
it 'only includes public projects' do
projects = Project.all.public_or_visible_to_user
expect(projects).to eq([public_project])
end
end
end
end end
...@@ -11,7 +11,7 @@ describe API::Users do ...@@ -11,7 +11,7 @@ describe API::Users do
let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 }
let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 }
describe "GET /users" do describe 'GET /users' do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authentication error" do
get api("/users") get api("/users")
...@@ -76,6 +76,12 @@ describe API::Users do ...@@ -76,6 +76,12 @@ describe API::Users do
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
it 'does not reveal the `is_admin` flag of the user' do
get api('/users', user)
expect(json_response.first.keys).not_to include 'is_admin'
end
end end
context "when admin" do context "when admin" do
...@@ -92,6 +98,7 @@ describe API::Users do ...@@ -92,6 +98,7 @@ describe API::Users do
expect(json_response.first.keys).to include 'two_factor_enabled' expect(json_response.first.keys).to include 'two_factor_enabled'
expect(json_response.first.keys).to include 'last_sign_in_at' expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at' expect(json_response.first.keys).to include 'confirmed_at'
expect(json_response.first.keys).to include 'is_admin'
end end
it "returns an array of external users" do it "returns an array of external users" do
......
...@@ -7,6 +7,38 @@ describe API::V3::Users do ...@@ -7,6 +7,38 @@ describe API::V3::Users do
let(:email) { create(:email, user: user) } let(:email) { create(:email, user: user) }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
describe 'GET /users' do
context 'when authenticated' do
it 'returns an array of users' do
get v3_api('/users', user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
user['username'] == username
end['username']).to eq(username)
end
end
context 'when authenticated as user' do
it 'does not reveal the `is_admin` flag of the user' do
get v3_api('/users', user)
expect(json_response.first.keys).not_to include 'is_admin'
end
end
context 'when authenticated as admin' do
it 'reveals the `is_admin` flag of the user' do
get v3_api('/users', admin)
expect(json_response.first.keys).to include 'is_admin'
end
end
end
describe 'GET /user/:id/keys' do describe 'GET /user/:id/keys' do
before { admin } before { admin }
......
...@@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do ...@@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(group) } it { expect(project.namespace).to eq(group) }
end end
context 'when transfer succeeds' do
before do
group.add_owner(user)
end
it 'sends notifications' do
expect_any_instance_of(NotificationService).to receive(:project_was_moved)
transfer_project(project, user, group)
end
it 'executes system hooks' do
expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks)
transfer_project(project, user, group)
end
end
context 'when transfer fails' do
let!(:original_path) { project_path(project) }
def attempt_project_transfer
expect do
transfer_project(project, user, group)
end.to raise_error(ActiveRecord::ActiveRecordError)
end
before do
group.add_owner(user)
expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR")
end
def project_path(project)
File.join(project.repository_storage_path, "#{project.path_with_namespace}.git")
end
def current_path
project_path(project)
end
it 'rolls back repo location' do
attempt_project_transfer
expect(Dir.exist?(original_path)).to be_truthy
expect(original_path).to eq current_path
end
it "doesn't send move notifications" do
expect_any_instance_of(NotificationService).not_to receive(:project_was_moved)
attempt_project_transfer
end
it "doesn't run system hooks" do
expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks)
attempt_project_transfer
end
end
context 'namespace -> no namespace' do context 'namespace -> no namespace' do
before do before do
@result = transfer_project(project, user, nil) @result = transfer_project(project, user, nil)
......
...@@ -13,9 +13,7 @@ shared_examples 'reportable note' do ...@@ -13,9 +13,7 @@ shared_examples 'reportable note' do
it 'dropdown has Edit, Report and Delete links' do it 'dropdown has Edit, Report and Delete links' do
dropdown = comment.find(more_actions_selector) dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
dropdown.click
dropdown.find('.dropdown-menu li', match: :first)
expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_button('Edit comment')
expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
...@@ -24,13 +22,16 @@ shared_examples 'reportable note' do ...@@ -24,13 +22,16 @@ shared_examples 'reportable note' do
it 'Report button links to a report page' do it 'Report button links to a report page' do
dropdown = comment.find(more_actions_selector) dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
dropdown.click
dropdown.find('.dropdown-menu li', match: :first)
dropdown.click_link('Report as abuse') dropdown.click_link('Report as abuse')
expect(find('#user_name')['value']).to match(note.author.username) expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note)) expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
end end
def open_dropdown(dropdown)
dropdown.click
dropdown.find('.dropdown-menu li', match: :first)
end
end end
require 'spec_helper'
describe 'projects/notes/_more_actions_dropdown', :view do
let(:author_user) { create(:user) }
let(:not_author_user) { create(:user) }
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let!(:note) { create(:note_on_issue, author: author_user, noteable: issue, project: project) }
before do
assign(:project, project)
end
it 'shows Report as abuse button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
expect(rendered).to have_link('Report as abuse')
end
it 'does not show the More actions button if not editable and current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: false, note: note
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
expect(rendered).to have_link('Report as abuse')
expect(rendered).to have_button('Edit comment')
expect(rendered).to have_link('Delete comment')
end
it 'shows Edit and Delete buttons if editable and current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note
expect(rendered).to have_button('Edit comment')
expect(rendered).to have_link('Delete comment')
end
end
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