Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
commit
1578ee9347
1
Gemfile
1
Gemfile
|
@ -260,6 +260,7 @@ gem 'premailer-rails', '~> 1.9.0'
|
|||
|
||||
# I18n
|
||||
gem 'ruby_parser', '~> 3.8', require: false
|
||||
gem 'rails-i18n', '~> 4.0.9'
|
||||
gem 'gettext_i18n_rails', '~> 1.8.0'
|
||||
gem 'gettext_i18n_rails_js', '~> 1.2.0'
|
||||
gem 'gettext', '~> 3.2.2', require: false, group: :development
|
||||
|
|
|
@ -646,6 +646,9 @@ GEM
|
|||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-i18n (4.0.9)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 4.0)
|
||||
railties (4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
activesupport (= 4.2.8)
|
||||
|
@ -1054,6 +1057,7 @@ DEPENDENCIES
|
|||
rack-proxy (~> 0.6.0)
|
||||
rails (= 4.2.8)
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 4.0.9)
|
||||
rainbow (~> 2.2)
|
||||
rblineprof (~> 0.3.6)
|
||||
rdoc (~> 4.2)
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
import CommitPipelinesTable from './pipelines_table';
|
||||
|
||||
Vue.use(VueResource);
|
||||
import commitPipelinesTable from './pipelines_table.vue';
|
||||
|
||||
/**
|
||||
* Commits View > Pipelines Tab > Pipelines Table.
|
||||
*
|
||||
* Renders Pipelines table in pipelines tab in the commits show view.
|
||||
* Used in:
|
||||
* - Commit details View > Pipelines Tab > Pipelines Table.
|
||||
* - 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)
|
||||
window.gl = window.gl || {};
|
||||
window.gl.CommitPipelinesTable = CommitPipelinesTable;
|
||||
|
||||
$(() => {
|
||||
gl.commits = gl.commits || {};
|
||||
gl.commits.pipelines = gl.commits.pipelines || {};
|
||||
const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
|
||||
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
|
||||
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
|
||||
const table = new CommitPipelinesTable({
|
||||
propsData: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
|
||||
},
|
||||
}).$mount();
|
||||
pipelineTableViewEl.appendChild(table.$el);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
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';
|
||||
|
||||
/**
|
||||
*
|
||||
* 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: {
|
||||
pipelinesTableComponent,
|
||||
errorState,
|
||||
emptyState,
|
||||
loadingIcon,
|
||||
},
|
||||
|
||||
/**
|
||||
* Accesses the DOM to provide the needed data.
|
||||
* Returns the necessary props to render `pipelines-table-component` component.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
data() {
|
||||
const store = new PipelineStore();
|
||||
|
||||
return {
|
||||
endpoint: null,
|
||||
helpPagePath: null,
|
||||
store,
|
||||
state: store.state,
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
isMakingRequest: false,
|
||||
updateGraphDropdown: 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() {
|
||||
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({
|
||||
resource: this.service,
|
||||
method: 'getPipelines',
|
||||
successCallback: this.successCallback,
|
||||
errorCallback: this.errorCallback,
|
||||
notificationCallback: this.setIsMakingRequest,
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
this.isLoading = true;
|
||||
this.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()) {
|
||||
this.poll.restart();
|
||||
} else {
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
|
||||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.poll.stop();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchPipelines() {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.service.getPipelines()
|
||||
.then(response => this.successCallback(response))
|
||||
.catch(() => this.errorCallback());
|
||||
},
|
||||
|
||||
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.isLoading = false;
|
||||
this.updateGraphDropdown = true;
|
||||
},
|
||||
|
||||
errorCallback() {
|
||||
this.hasError = true;
|
||||
this.isLoading = false;
|
||||
this.updateGraphDropdown = false;
|
||||
},
|
||||
|
||||
setIsMakingRequest(isMakingRequest) {
|
||||
this.isMakingRequest = isMakingRequest;
|
||||
|
||||
if (isMakingRequest) {
|
||||
this.updateGraphDropdown = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
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>
|
||||
`,
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
<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 {
|
|||
initDomElements() {
|
||||
this.$page = $('.page-with-sidebar');
|
||||
this.$sidebar = $('.right-sidebar');
|
||||
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
|
||||
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
|
||||
this.$bulkEditSubmitBtn = $('.update-selected-issues');
|
||||
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
|
||||
|
@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar {
|
|||
toggleSidebarDisplay(show) {
|
||||
this.$page.toggleClass(SIDEBAR_EXPANDED_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_COLLAPSED_CLASS, !show);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
initialTaskStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -105,6 +110,7 @@ export default {
|
|||
updatedAt: this.updatedAt,
|
||||
updatedByName: this.updatedByName,
|
||||
updatedByPath: this.updatedByPath,
|
||||
taskStatus: this.initialTaskStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -37,18 +37,7 @@
|
|||
});
|
||||
},
|
||||
taskStatus() {
|
||||
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\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('');
|
||||
}
|
||||
this.updateTaskStatusText();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -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() {
|
||||
this.renderGFM();
|
||||
this.updateTaskStatusText();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
updatedAt: this.updatedAt,
|
||||
updatedByName: this.updatedByName,
|
||||
updatedByPath: this.updatedByPath,
|
||||
initialTaskStatus: this.initialTaskStatus,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,23 +1,6 @@
|
|||
export default class Store {
|
||||
constructor({
|
||||
titleHtml,
|
||||
titleText,
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
updatedAt,
|
||||
updatedByName,
|
||||
updatedByPath,
|
||||
}) {
|
||||
this.state = {
|
||||
titleHtml,
|
||||
titleText,
|
||||
descriptionHtml,
|
||||
descriptionText,
|
||||
taskStatus: '',
|
||||
updatedAt,
|
||||
updatedByName,
|
||||
updatedByPath,
|
||||
};
|
||||
constructor(initialState) {
|
||||
this.state = initialState;
|
||||
this.formState = {
|
||||
title: '',
|
||||
confidential: false,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,10 +3,12 @@
|
|||
/* global Flash */
|
||||
/* global notes */
|
||||
|
||||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import './breakpoints';
|
||||
import './flash';
|
||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||
import commitPipelinesTable from './commit/pipelines/pipelines_table.vue';
|
||||
|
||||
/* eslint-disable max-len */
|
||||
// MergeRequestTabs
|
||||
|
@ -233,11 +235,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
}
|
||||
|
||||
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
|
||||
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
|
||||
document.querySelector('#commit-pipeline-table-view')
|
||||
.appendChild(this.commitPipelinesTable.$el);
|
||||
pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
|
||||
}
|
||||
|
||||
loadDiff(source) {
|
||||
|
|
|
@ -187,7 +187,7 @@ const normalizeNewlines = function(str) {
|
|||
if ($textarea.val() !== '') {
|
||||
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) {
|
||||
myLastNoteEditBtn = myLastNote.find('.js-note-edit');
|
||||
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
/* eslint-disable no-new, no-alert */
|
||||
/* global Flash */
|
||||
import '~/flash';
|
||||
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltipMixin from '../../vue_shared/mixins/tooltip';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -11,53 +11,42 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
confirmActionMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
iconClass() {
|
||||
return `fa fa-${this.icon}`;
|
||||
},
|
||||
|
||||
buttonClass() {
|
||||
return `btn has-tooltip ${this.cssClass}`;
|
||||
return `btn ${this.cssClass}`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
|
||||
|
@ -66,21 +55,11 @@ export default {
|
|||
this.makeRequest();
|
||||
}
|
||||
},
|
||||
|
||||
makeRequest() {
|
||||
this.isLoading = true;
|
||||
|
||||
$(this.$el).tooltip('destroy');
|
||||
|
||||
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.');
|
||||
});
|
||||
$(this.$refs.tooltip).tooltip('destroy');
|
||||
eventHub.$emit('postAction', this.endpoint);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -95,10 +74,12 @@ export default {
|
|||
:aria-label="title"
|
||||
data-container="body"
|
||||
data-placement="top"
|
||||
ref="tooltip"
|
||||
:disabled="isLoading">
|
||||
<i
|
||||
:class="iconClass"
|
||||
aria-hidden="true" />
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
<loading-icon v-if="isLoading" />
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<script>
|
||||
import Visibility from 'visibilityjs';
|
||||
import PipelinesService from '../services/pipelines_service';
|
||||
import eventHub from '../event_hub';
|
||||
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
|
||||
import pipelinesMixin from '../mixins/pipelines';
|
||||
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 navigationControls from './nav_controls.vue';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -20,13 +14,12 @@
|
|||
},
|
||||
components: {
|
||||
tablePagination,
|
||||
pipelinesTableComponent,
|
||||
emptyState,
|
||||
errorState,
|
||||
navigationTabs,
|
||||
navigationControls,
|
||||
loadingIcon,
|
||||
},
|
||||
mixins: [
|
||||
pipelinesMixin,
|
||||
],
|
||||
data() {
|
||||
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
|
||||
|
||||
|
@ -47,11 +40,6 @@
|
|||
state: this.store.state,
|
||||
apiScope: 'all',
|
||||
pagenum: 1,
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
isMakingRequest: false,
|
||||
updateGraphDropdown: false,
|
||||
hasMadeRequest: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -62,9 +50,6 @@
|
|||
const scope = gl.utils.getParameterByName('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
|
||||
|
@ -106,7 +91,6 @@
|
|||
this.state.pipelines.length &&
|
||||
this.state.pageInfo.total > this.state.pageInfo.perPage;
|
||||
},
|
||||
|
||||
hasCiEnabled() {
|
||||
return this.hasCi !== undefined;
|
||||
},
|
||||
|
@ -129,37 +113,7 @@
|
|||
},
|
||||
created() {
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
|
||||
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');
|
||||
this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -174,15 +128,6 @@
|
|||
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) {
|
||||
const response = {
|
||||
headers: resp.headers,
|
||||
|
@ -190,33 +135,14 @@
|
|||
};
|
||||
|
||||
this.store.storeCount(response.body.count);
|
||||
this.store.storePipelines(response.body.pipelines);
|
||||
this.store.storePagination(response.headers);
|
||||
|
||||
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;
|
||||
}
|
||||
this.setCommonData(response.body.pipelines);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cssClass">
|
||||
|
||||
<div
|
||||
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
|
||||
v-if="!isLoading && !shouldRenderEmptyState">
|
||||
|
@ -274,7 +200,6 @@
|
|||
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
:service="service"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loadingIcon,
|
||||
|
@ -31,17 +27,9 @@
|
|||
|
||||
$(this.$refs.tooltip).tooltip('destroy');
|
||||
|
||||
this.service.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.');
|
||||
});
|
||||
eventHub.$emit('postAction', endpoint);
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
if (action.playable === undefined) {
|
||||
return false;
|
||||
|
|
|
@ -12,10 +12,6 @@
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
updateGraphDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -57,7 +53,6 @@
|
|||
v-for="model in pipelines"
|
||||
:key="model.id"
|
||||
:pipeline="model"
|
||||
:service="service"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
/>
|
||||
</div>
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
/* eslint-disable no-param-reassign */
|
||||
import asyncButtonComponent from '../../pipelines/components/async_button.vue';
|
||||
import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue';
|
||||
import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue';
|
||||
import ciBadge from './ci_badge_link.vue';
|
||||
import pipelineStage from '../../pipelines/components/stage.vue';
|
||||
import pipelineUrl from '../../pipelines/components/pipeline_url.vue';
|
||||
import pipelinesTimeago from '../../pipelines/components/time_ago.vue';
|
||||
import commitComponent from './commit.vue';
|
||||
import asyncButtonComponent from './async_button.vue';
|
||||
import pipelinesActionsComponent from './pipelines_actions.vue';
|
||||
import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
|
||||
import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
|
||||
import pipelineStage from './stage.vue';
|
||||
import pipelineUrl from './pipeline_url.vue';
|
||||
import pipelinesTimeago from './time_ago.vue';
|
||||
import commitComponent from '../../vue_shared/components/commit.vue';
|
||||
|
||||
/**
|
||||
* Pipeline table row.
|
||||
|
@ -20,10 +20,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
updateGraphDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -271,7 +267,6 @@ export default {
|
|||
<pipelines-actions-component
|
||||
v-if="pipeline.details.manual_actions.length"
|
||||
:actions="pipeline.details.manual_actions"
|
||||
:service="service"
|
||||
/>
|
||||
|
||||
<pipelines-artifacts-component
|
||||
|
@ -282,7 +277,6 @@ export default {
|
|||
|
||||
<async-button-component
|
||||
v-if="pipeline.flags.retryable"
|
||||
:service="service"
|
||||
:endpoint="pipeline.retry_path"
|
||||
css-class="js-pipelines-retry-button btn-default btn-retry"
|
||||
title="Retry"
|
||||
|
@ -291,7 +285,6 @@ export default {
|
|||
|
||||
<async-button-component
|
||||
v-if="pipeline.flags.cancelable"
|
||||
:service="service"
|
||||
:endpoint="pipeline.cancel_path"
|
||||
css-class="js-pipelines-cancel-button btn-remove"
|
||||
title="Cancel"
|
|
@ -0,0 +1,103 @@
|
|||
/* global Flash */
|
||||
import '~/flash';
|
||||
import Visibility from 'visibilityjs';
|
||||
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 {
|
||||
components: {
|
||||
pipelinesTableComponent,
|
||||
errorState,
|
||||
emptyState,
|
||||
loadingIcon,
|
||||
},
|
||||
computed: {
|
||||
shouldRenderErrorState() {
|
||||
return this.hasError && !this.isLoading;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
isMakingRequest: false,
|
||||
updateGraphDropdown: false,
|
||||
hasMadeRequest: false,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'getPipelines',
|
||||
data: this.requestData ? this.requestData : undefined,
|
||||
successCallback: this.successCallback,
|
||||
errorCallback: this.errorCallback,
|
||||
notificationCallback: this.setIsMakingRequest,
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
this.isLoading = true;
|
||||
this.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()) {
|
||||
this.poll.restart();
|
||||
} else {
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
|
||||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
},
|
||||
destroyed() {
|
||||
this.poll.stop();
|
||||
},
|
||||
methods: {
|
||||
fetchPipelines() {
|
||||
if (!this.isMakingRequest) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service.getPipelines(this.requestData)
|
||||
.then(response => this.successCallback(response))
|
||||
.catch(() => this.errorCallback());
|
||||
}
|
||||
},
|
||||
setCommonData(pipelines) {
|
||||
this.store.storePipelines(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;
|
||||
}
|
||||
},
|
||||
postAction(endpoint) {
|
||||
this.service.postAction(endpoint)
|
||||
.then(() => eventHub.$emit('refreshPipelines'))
|
||||
.catch(() => new Flash('An error occured while making the request.'));
|
||||
},
|
||||
},
|
||||
};
|
|
@ -254,7 +254,7 @@
|
|||
}
|
||||
|
||||
.landing {
|
||||
margin-bottom: $gl-padding;
|
||||
margin: $gl-padding auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
|
|
@ -236,9 +236,6 @@
|
|||
width: 35px;
|
||||
background-color: $white-light;
|
||||
border: none;
|
||||
position: static;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
|
||||
|
|
|
@ -97,17 +97,19 @@
|
|||
|
||||
.issues-bulk-update.right-sidebar {
|
||||
@include maintain-sidebar-dimensions;
|
||||
transition: right $sidebar-transition-duration;
|
||||
right: -$gutter-width;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
transition: width $sidebar-transition-duration;
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: 0;
|
||||
width: $gutter-width;
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: -$gutter-width;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
|
||||
.block {
|
||||
padding: 16px 0;
|
||||
|
@ -118,5 +120,6 @@
|
|||
|
||||
.issuable-sidebar {
|
||||
padding: 0 3px;
|
||||
width: calc(100% + 35px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
position: relative;
|
||||
|
||||
.landing {
|
||||
margin-top: 10px;
|
||||
margin-top: 0;
|
||||
|
||||
.inner-content {
|
||||
white-space: normal;
|
||||
|
|
|
@ -90,8 +90,6 @@
|
|||
}
|
||||
|
||||
.explore-groups.landing {
|
||||
margin-top: 10px;
|
||||
|
||||
.inner-content {
|
||||
padding: 0;
|
||||
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding: $gl-padding-top $gl-padding $gl-padding;
|
||||
background-color: $white-light;
|
||||
|
|
|
@ -33,7 +33,8 @@ class EventsFinder
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def by_action(events)
|
||||
|
|
|
@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder
|
|||
private
|
||||
|
||||
def init_collection
|
||||
only_owned = options.fetch(:only_owned, false)
|
||||
only_shared = options.fetch(:only_shared, false)
|
||||
projects = if current_user
|
||||
collection_with_user
|
||||
else
|
||||
collection_without_user
|
||||
end
|
||||
|
||||
projects = []
|
||||
union(projects)
|
||||
end
|
||||
|
||||
if current_user
|
||||
if group.users.include?(current_user)
|
||||
projects << group.projects unless only_shared
|
||||
projects << group.shared_projects unless only_owned
|
||||
def collection_with_user
|
||||
if group.users.include?(current_user)
|
||||
if only_shared?
|
||||
[shared_projects]
|
||||
elsif only_owned?
|
||||
[owned_projects]
|
||||
else
|
||||
unless only_shared
|
||||
projects << group.projects.visible_to_user(current_user)
|
||||
projects << group.projects.public_to_user(current_user)
|
||||
end
|
||||
|
||||
unless only_owned
|
||||
projects << group.shared_projects.visible_to_user(current_user)
|
||||
projects << group.shared_projects.public_to_user(current_user)
|
||||
end
|
||||
[shared_projects, owned_projects]
|
||||
end
|
||||
else
|
||||
projects << group.projects.public_only unless only_shared
|
||||
projects << group.shared_projects.public_only unless only_owned
|
||||
if only_shared?
|
||||
[shared_projects.public_or_visible_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
|
||||
|
||||
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
|
||||
|
||||
def union(items)
|
||||
find_union(items, Project)
|
||||
if items.one?
|
||||
items.first
|
||||
else
|
||||
find_union(items, Project)
|
||||
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
|
||||
|
|
|
@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder
|
|||
end
|
||||
|
||||
def execute
|
||||
items = init_collection
|
||||
items = items.map do |item|
|
||||
item = by_ids(item)
|
||||
item = by_personal(item)
|
||||
item = by_starred(item)
|
||||
item = by_trending(item)
|
||||
item = by_visibilty_level(item)
|
||||
item = by_tags(item)
|
||||
item = by_search(item)
|
||||
by_archived(item)
|
||||
end
|
||||
items = union(items)
|
||||
sort(items)
|
||||
collection = init_collection
|
||||
collection = by_ids(collection)
|
||||
collection = by_personal(collection)
|
||||
collection = by_starred(collection)
|
||||
collection = by_trending(collection)
|
||||
collection = by_visibilty_level(collection)
|
||||
collection = by_tags(collection)
|
||||
collection = by_search(collection)
|
||||
collection = by_archived(collection)
|
||||
|
||||
sort(collection)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_collection
|
||||
projects = []
|
||||
|
||||
if params[:owned].present?
|
||||
projects << current_user.owned_projects if current_user
|
||||
if current_user
|
||||
collection_with_user
|
||||
else
|
||||
projects << current_user.authorized_projects if current_user
|
||||
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
|
||||
collection_without_user
|
||||
end
|
||||
end
|
||||
|
||||
projects
|
||||
def collection_with_user
|
||||
if owned_projects?
|
||||
current_user.owned_projects
|
||||
else
|
||||
if private_only?
|
||||
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
|
||||
|
||||
def private_only?
|
||||
params[:non_public].present?
|
||||
end
|
||||
|
||||
def by_ids(items)
|
||||
|
|
|
@ -68,7 +68,7 @@ module ApplicationHelper
|
|||
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 =
|
||||
if user_or_email.is_a?(User)
|
||||
user_or_email
|
||||
|
@ -77,7 +77,7 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
if user
|
||||
user.avatar_url(size: size) || default_avatar
|
||||
user.avatar_url(size: size, only_path: only_path) || default_avatar
|
||||
else
|
||||
gravatar_icon(user_or_email, size, scale)
|
||||
end
|
||||
|
|
|
@ -138,8 +138,8 @@ module IssuablesHelper
|
|||
end
|
||||
|
||||
output << " ".html_safe
|
||||
output << content_tag(:span, issuable.task_status, 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 if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
|
||||
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
|
||||
|
||||
output
|
||||
end
|
||||
|
@ -216,7 +216,8 @@ module IssuablesHelper
|
|||
initialTitleHtml: markdown_field(issuable, :title),
|
||||
initialTitleText: issuable.title,
|
||||
initialDescriptionHtml: markdown_field(issuable, :description),
|
||||
initialDescriptionText: issuable.description
|
||||
initialDescriptionText: issuable.description,
|
||||
initialTaskStatus: issuable.task_status
|
||||
}
|
||||
|
||||
data.merge!(updated_at_by(issuable))
|
||||
|
|
|
@ -266,20 +266,49 @@ class Project < ActiveRecord::Base
|
|||
|
||||
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",
|
||||
# they are only available to team members. This scope returns projects where
|
||||
# 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)
|
||||
return with_feature_enabled(feature) if user.try(:admin?)
|
||||
visible = [nil, ProjectFeature::ENABLED]
|
||||
|
||||
unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
|
||||
return unconditional if user.nil?
|
||||
if user&.admin?
|
||||
with_feature_enabled(feature)
|
||||
elsif user
|
||||
column = ProjectFeature.quoted_access_level_column(feature)
|
||||
|
||||
conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
|
||||
authorized = user.authorized_projects.merge(conditional.reorder(nil))
|
||||
authorized = user.project_authorizations.select(1).
|
||||
where('project_authorizations.project_id = projects.id')
|
||||
|
||||
union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
|
||||
where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
|
||||
with_project_feature.
|
||||
where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
|
||||
visible,
|
||||
ProjectFeature::PRIVATE,
|
||||
authorized)
|
||||
else
|
||||
with_feature_access_level(feature, visible)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
|||
|
||||
"#{feature}_access_level".to_sym
|
||||
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
|
||||
|
||||
# Default scopes force us to unscope here since a service may need to check
|
||||
|
|
|
@ -70,7 +70,7 @@ module ChatMessage
|
|||
end
|
||||
|
||||
def branch_link
|
||||
"`[#{ref}](#{branch_url})`"
|
||||
"[#{ref}](#{branch_url})"
|
||||
end
|
||||
|
||||
def project_link
|
||||
|
|
|
@ -61,7 +61,7 @@ module ChatMessage
|
|||
end
|
||||
|
||||
def removed_branch_message
|
||||
"#{user_name} removed #{ref_type} `#{ref}` from #{project_link}"
|
||||
"#{user_name} removed #{ref_type} #{ref} from #{project_link}"
|
||||
end
|
||||
|
||||
def push_message
|
||||
|
@ -102,7 +102,7 @@ module ChatMessage
|
|||
end
|
||||
|
||||
def branch_link
|
||||
"`[#{ref}](#{branch_url})`"
|
||||
"[#{ref}](#{branch_url})"
|
||||
end
|
||||
|
||||
def project_link
|
||||
|
|
|
@ -12,87 +12,121 @@ module Projects
|
|||
TransferError = Class.new(StandardError)
|
||||
|
||||
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.'
|
||||
end
|
||||
unless allowed_transfer?(current_user, project, new_namespace)
|
||||
|
||||
unless allowed_transfer?(current_user, project)
|
||||
raise TransferError, 'Transfer failed, please contact an admin.'
|
||||
end
|
||||
transfer(project, new_namespace)
|
||||
|
||||
transfer(project)
|
||||
|
||||
true
|
||||
rescue Projects::TransferService::TransferError => ex
|
||||
project.reload
|
||||
project.errors.add(:new_namespace, ex.message)
|
||||
false
|
||||
end
|
||||
|
||||
def transfer(project, new_namespace)
|
||||
old_namespace = project.namespace
|
||||
private
|
||||
|
||||
def transfer(project)
|
||||
@old_path = project.path_with_namespace
|
||||
@old_group = project.group
|
||||
@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)).exists?
|
||||
raise TransferError.new("Project with same path in target namespace already exists")
|
||||
end
|
||||
|
||||
if project.has_container_registry_tags?
|
||||
# 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')
|
||||
end
|
||||
|
||||
attempt_transfer_transaction
|
||||
end
|
||||
|
||||
def attempt_transfer_transaction
|
||||
Project.transaction do
|
||||
old_path = project.path_with_namespace
|
||||
old_group = project.group
|
||||
new_path = File.join(new_namespace.try(:full_path) || '', project.path)
|
||||
project.expire_caches_before_rename(@old_path)
|
||||
|
||||
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
|
||||
raise TransferError.new("Project with same path in target namespace already exists")
|
||||
end
|
||||
|
||||
if project.has_container_registry_tags?
|
||||
# we currently doesn'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')
|
||||
end
|
||||
|
||||
project.expire_caches_before_rename(old_path)
|
||||
|
||||
# Apply new namespace id and visibility level
|
||||
project.namespace = new_namespace
|
||||
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
|
||||
project.save!
|
||||
update_namespace_and_visibility(@new_namespace)
|
||||
|
||||
# Notifications
|
||||
project.send_move_instructions(old_path)
|
||||
project.send_move_instructions(@old_path)
|
||||
|
||||
# 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')
|
||||
end
|
||||
|
||||
# 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
|
||||
Labels::TransferService.new(current_user, old_group, project).execute
|
||||
Labels::TransferService.new(current_user, @old_group, project).execute
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
refresh_permissions(old_namespace, new_namespace)
|
||||
|
||||
true
|
||||
rescue Exception # rubocop:disable Lint/RescueException
|
||||
rollback_side_effects
|
||||
raise
|
||||
ensure
|
||||
refresh_permissions
|
||||
end
|
||||
|
||||
def allowed_transfer?(current_user, project, namespace)
|
||||
namespace &&
|
||||
def allowed_transfer?(current_user, project)
|
||||
@new_namespace &&
|
||||
can?(current_user, :change_namespace, project) &&
|
||||
namespace.id != project.namespace_id &&
|
||||
current_user.can?(:create_projects, namespace)
|
||||
@new_namespace.id != project.namespace_id &&
|
||||
current_user.can?(:create_projects, @new_namespace)
|
||||
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
|
||||
# the namespaces.
|
||||
user_ids = old_namespace.user_ids_for_project_authorizations |
|
||||
new_namespace.user_ids_for_project_authorizations
|
||||
user_ids = @old_namespace.user_ids_for_project_authorizations |
|
||||
@new_namespace.user_ids_for_project_authorizations
|
||||
|
||||
UserProjectAccessChangedService.new(user_ids).execute
|
||||
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
|
||||
|
|
|
@ -325,6 +325,10 @@
|
|||
= f.label :prometheus_metrics_enabled do
|
||||
= f.check_box :prometheus_metrics_enabled
|
||||
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
|
||||
%legend Background Jobs
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
%tbody
|
||||
%tr
|
||||
%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;" }
|
||||
- if commit.author
|
||||
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
|
||||
|
@ -76,7 +76,7 @@
|
|||
%tbody
|
||||
%tr
|
||||
%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;" }
|
||||
- if commit.committer
|
||||
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
|
||||
|
@ -100,7 +100,7 @@
|
|||
triggered by
|
||||
- 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" }
|
||||
%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;" }
|
||||
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
|
||||
= @pipeline.user.name
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
%tbody
|
||||
%tr
|
||||
%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;" }
|
||||
- if commit.author
|
||||
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
|
||||
|
@ -76,7 +76,7 @@
|
|||
%tbody
|
||||
%tr
|
||||
%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;" }
|
||||
- if commit.committer
|
||||
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
|
||||
|
@ -100,7 +100,7 @@
|
|||
triggered by
|
||||
- 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" }
|
||||
%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;" }
|
||||
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
|
||||
= @pipeline.user.name
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
%li
|
||||
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
|
||||
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
|
||||
.toolbar-group
|
||||
= markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
.dropzone
|
||||
.dropzone-previews.blob-upload-dropzone-previews
|
||||
%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 & drop or %{upload_link}') % { upload_link: upload_link }
|
||||
#{ dropzone_text.html_safe }
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
= label_tag 'start_branch', branch_label, class: 'control-label'
|
||||
.col-sm-10
|
||||
= 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)
|
||||
= render 'shared/new_merge_request_checkbox'
|
||||
|
|
|
@ -5,13 +5,6 @@
|
|||
- can_update_issue = can?(current_user, :update_issue, @issue)
|
||||
- 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
|
||||
.issuable-header
|
||||
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
|
||||
|
@ -26,6 +19,7 @@
|
|||
= icon('angle-double-left')
|
||||
|
||||
.issuable-meta
|
||||
= confidential_icon(@issue)
|
||||
= issuable_meta(@issue, @project, "Issue")
|
||||
|
||||
.issuable-actions
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
.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
|
||||
= icon('ellipsis-v', class: 'icon')
|
||||
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
|
||||
%li
|
||||
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
|
||||
%li.divider
|
||||
%li
|
||||
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
|
||||
Report as abuse
|
||||
- if note_editable
|
||||
%li
|
||||
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
|
||||
%span.text-danger Delete comment
|
||||
- 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
|
||||
= icon('ellipsis-v', class: 'icon')
|
||||
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
|
||||
- if note_editable
|
||||
%li
|
||||
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
|
||||
%li.divider
|
||||
- unless is_current_user
|
||||
%li
|
||||
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
|
||||
Report as abuse
|
||||
- if note_editable
|
||||
%li
|
||||
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
|
||||
%span.text-danger Delete comment
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
.form-group
|
||||
.col-md-9
|
||||
= 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
|
||||
.form-group
|
||||
.col-md-9
|
||||
= 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
|
||||
.form-group
|
||||
.col-md-9
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- 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" }
|
||||
.issuable-sidebar
|
||||
.issuable-sidebar.hidden
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
|
||||
.block
|
||||
.filter-item.inline.update-issues-btn.pull-left
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix an email parsing bug where brackets would be inserted in emails from some Outlook clients
|
||||
merge_request: 9045
|
||||
author: jneen
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Rollback project repo move if there is an error in Projects::TransferService
|
||||
merge_request: 11877
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Reinstate is_admin flag in users api when authenticated user is an admin
|
||||
merge_request: 12211
|
||||
author: rickettm
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Fix for cut & pasted images not working
|
||||
merge_request:
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Make confidential issues more obviously confidential
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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__)
|
|||
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
||||
|
||||
# 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
|
||||
require 'bootsnap'
|
||||
|
|
|
@ -66,5 +66,14 @@ module.exports = function(config) {
|
|||
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);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# 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 @@
|
|||
Available since [Omnibus GitLab 8.17][1132]. For installations from source
|
||||
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:
|
||||
|
||||
|
|
|
@ -110,6 +110,14 @@ To disable the monitoring of Kubernetes:
|
|||
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
|
||||
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
|
||||
|
||||
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.
|
|||
|
||||
### 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)
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ following locations:
|
|||
- [Labels](labels.md)
|
||||
- [Merge Requests](merge_requests.md)
|
||||
- [Milestones](milestones.md)
|
||||
- [Open source license templates](templates/licenses.md)
|
||||
- [Namespaces](namespaces.md)
|
||||
- [Notes](notes.md) (comments)
|
||||
- [Notification settings](notification_settings.md)
|
||||
- [Open source license templates](templates/licenses.md)
|
||||
- [Pipelines](pipelines.md)
|
||||
- [Pipeline Triggers](pipeline_triggers.md)
|
||||
- [Pipeline Schedules](pipeline_schedules.md)
|
||||
|
|
|
@ -62,6 +62,7 @@ GET /users
|
|||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||
"web_url": "http://localhost:3000/john_smith",
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
|
@ -94,6 +95,7 @@ GET /users
|
|||
"avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg",
|
||||
"web_url": "http://localhost:3000/jack_smith",
|
||||
"created_at": "2012-05-23T08:01:01Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
|
@ -197,6 +199,7 @@ Parameters:
|
|||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
|
||||
"web_url": "http://localhost:3000/john_smith",
|
||||
"created_at": "2012-05-23T08:00:58Z",
|
||||
"is_admin": false,
|
||||
"bio": null,
|
||||
"location": null,
|
||||
"skype": "",
|
||||
|
|
|
@ -86,6 +86,79 @@ 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.
|
||||
|
||||
## 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:
|
||||
|
||||
- PostgreSQL
|
||||
- MySQL/MariaDB
|
||||
|
||||
We **highly recommend** the use of PostgreSQL instead of MySQL/MariaDB as not all
|
||||
features of GitLab may work with MySQL/MariaDB:
|
||||
|
||||
1. MySQL support for subgroups was [dropped with GitLab 9.3][post].
|
||||
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
|
||||
[migrate to PostgreSQL](../update/mysql_to_postgresql.md) instead.
|
||||
|
||||
[30472]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472
|
||||
[zero]: ../update/README.md#upgrading-without-downtime
|
||||
[post]: https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#dropping-support-for-subgroups-in-mysql
|
||||
|
||||
### PostgreSQL Requirements
|
||||
|
||||
As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are
|
||||
not supported. We highly recommend users to use at least PostgreSQL 9.6 as this
|
||||
is the PostgreSQL version used for development and testing.
|
||||
|
||||
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
|
||||
GitLab database. This extension can be enabled (using a PostgreSQL super user)
|
||||
by running the following query for every database:
|
||||
|
||||
```
|
||||
CREATE EXTENSION pg_trgm;
|
||||
```
|
||||
|
||||
On some systems you may need to install an additional package (e.g.
|
||||
`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 stores all user sessions and the background task queue.
|
||||
The storage requirements for Redis are minimal, about 25kB per user.
|
||||
Sidekiq processes the background jobs with a multithreaded process.
|
||||
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
|
||||
On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory.
|
||||
|
||||
## Prometheus and its exporters
|
||||
|
||||
As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related
|
||||
exporters are enabled by default, to enable easy and in depth monitoring of
|
||||
GitLab. Approximately 200MB of memory will be consumed by these processes, with
|
||||
default settings.
|
||||
|
||||
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).
|
||||
|
||||
## GitLab Runner
|
||||
|
||||
We strongly advise against installing GitLab Runner on the same machine you plan
|
||||
|
@ -106,72 +179,6 @@ 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
|
||||
|
||||
We currently support the following databases:
|
||||
|
||||
- PostgreSQL
|
||||
- MySQL/MariaDB
|
||||
|
||||
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
|
||||
the right features to support nested groups in an efficient manner; see
|
||||
<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information
|
||||
about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication).
|
||||
Existing users using GitLab with MySQL/MariaDB are advised to
|
||||
migrate to PostgreSQL instead.
|
||||
|
||||
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).
|
||||
|
||||
### PostgreSQL Requirements
|
||||
|
||||
As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are
|
||||
not supported. We highly recommend users to use at least PostgreSQL 9.6 as this
|
||||
is the PostgreSQL version used for development and testing.
|
||||
|
||||
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
|
||||
GitLab database. This extension can be enabled (using a PostgreSQL super user)
|
||||
by running the following query for every database:
|
||||
|
||||
```
|
||||
CREATE EXTENSION pg_trgm;
|
||||
```
|
||||
|
||||
On some systems you may need to install an additional package (e.g.
|
||||
`postgresql-contrib`) for this extension to become available.
|
||||
|
||||
## Redis and Sidekiq
|
||||
|
||||
Redis stores all user sessions and the background task queue.
|
||||
The storage requirements for Redis are minimal, about 25kB per user.
|
||||
Sidekiq processes the background jobs with a multithreaded process.
|
||||
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
|
||||
On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory.
|
||||
|
||||
## Prometheus and its exporters
|
||||
|
||||
As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related
|
||||
exporters are enabled by default, to enable easy and in depth monitoring of
|
||||
GitLab. Approximately 200MB of memory will be consumed by these processes, with
|
||||
default settings.
|
||||
|
||||
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).
|
||||
|
||||
## 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).
|
||||
|
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
<!-- 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
|
||||
|
||||
- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# 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
|
||||
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:
|
|||
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
|
||||
[permissions]: ../../permissions.md#group
|
||||
[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.
|
|||
|
||||
---
|
||||
|
||||
While inside the issue, you can see a persistent dark banner at the top of the
|
||||
screen.
|
||||
Likewise, while inside the issue, you can see the eye-slash icon right next to
|
||||
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)
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 14 KiB |
|
@ -43,11 +43,14 @@ module API
|
|||
expose :external
|
||||
end
|
||||
|
||||
class UserWithPrivateDetails < UserPublic
|
||||
expose :private_token
|
||||
class UserWithAdmin < UserPublic
|
||||
expose :admin?, as: :is_admin
|
||||
end
|
||||
|
||||
class UserWithPrivateDetails < UserWithAdmin
|
||||
expose :private_token
|
||||
end
|
||||
|
||||
class Email < Grape::Entity
|
||||
expose :id, :email
|
||||
end
|
||||
|
|
|
@ -71,11 +71,16 @@ module API
|
|||
end
|
||||
|
||||
#
|
||||
# Discover user by ssh key
|
||||
# Discover user by ssh key or user id
|
||||
#
|
||||
get "/discover" do
|
||||
key = Key.find(params[:key_id])
|
||||
present key.user, with: Entities::UserSafe
|
||||
if params[:key_id]
|
||||
key = Key.find(params[:key_id])
|
||||
user = key.user
|
||||
elsif params[:user_id]
|
||||
user = User.find_by(id: params[:user_id])
|
||||
end
|
||||
present user, with: Entities::UserSafe
|
||||
end
|
||||
|
||||
get "/check" do
|
||||
|
|
|
@ -59,7 +59,7 @@ module API
|
|||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -17,6 +17,13 @@ module Gitlab
|
|||
def filter_replies!
|
||||
document.xpath('//blockquote').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
|
||||
|
||||
def filtered_html
|
||||
|
|
|
@ -5,8 +5,16 @@ module Gitlab
|
|||
module Prometheus
|
||||
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?
|
||||
@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
|
||||
|
||||
def registry
|
||||
|
@ -36,6 +44,12 @@ module Gitlab
|
|||
NullMetric.new
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prometheus_metrics_enabled_unmemoized
|
||||
metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,18 +13,8 @@ module Gitlab
|
|||
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
|
||||
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
|
||||
|
||||
scope :public_to_user, -> (user) do
|
||||
if user
|
||||
if user.admin?
|
||||
all
|
||||
elsif !user.external?
|
||||
public_and_internal_only
|
||||
else
|
||||
public_only
|
||||
end
|
||||
else
|
||||
public_only
|
||||
end
|
||||
scope :public_to_user, -> (user = nil) do
|
||||
where(visibility_level: VisibilityLevel.levels_for_user(user))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -35,6 +25,18 @@ module Gitlab
|
|||
class << self
|
||||
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
|
||||
string_options.keys
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\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: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -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}"
|
||||
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"
|
||||
msgstr "Ramas"
|
||||
|
||||
|
@ -945,6 +951,9 @@ msgstr "Subir nuevo archivo"
|
|||
msgid "Upload file"
|
||||
msgstr "Subir archivo"
|
||||
|
||||
msgid "UploadLink|click to upload"
|
||||
msgstr "Hacer clic para subir"
|
||||
|
||||
msgid "Use your global notification setting"
|
||||
msgstr "Utiliza tu configuración de notificación global"
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: gitlab 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-06-15 21:59-0500\n"
|
||||
"PO-Revision-Date: 2017-06-15 21:59-0500\n"
|
||||
"POT-Creation-Date: 2017-06-19 15:13-0500\n"
|
||||
"PO-Revision-Date: 2017-06-19 15:13-0500\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
@ -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}"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchSwitcherPlaceholder|Search branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchSwitcherTitle|Switch branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branches"
|
||||
msgstr ""
|
||||
|
||||
|
@ -946,6 +952,9 @@ msgstr ""
|
|||
msgid "Upload file"
|
||||
msgstr ""
|
||||
|
||||
msgid "UploadLink|click to upload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use your global notification setting"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -16,6 +16,21 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
gitlab_sign_in user
|
||||
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
|
||||
before do
|
||||
enable_bulk_update
|
||||
|
@ -398,4 +413,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
visit namespace_project_issues_path(project.namespace, project)
|
||||
click_button 'Edit Issues'
|
||||
end
|
||||
|
||||
def disable_bulk_update
|
||||
click_button 'Cancel'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,15 +19,4 @@ describe 'Reportable note on snippets', :feature, :js do
|
|||
|
||||
it_behaves_like 'reportable note'
|
||||
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
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
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
|
|||
end
|
||||
|
||||
describe 'avatar_icon' do
|
||||
it 'returns an url for the avatar' do
|
||||
user = create(:user, avatar: File.open(uploaded_image_temp_path))
|
||||
let(: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)
|
||||
avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif"
|
||||
before do
|
||||
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
|
||||
|
||||
context 'when the GitLab instance is at a relative URL' do
|
||||
before do
|
||||
stub_config_setting(relative_url_root: '/gitlab')
|
||||
# Must be stubbed after the stub above, and separately
|
||||
stub_config_setting(url: Settings.send(:build_gitlab_url))
|
||||
end
|
||||
|
||||
it 'returns a relative URL with the correct prefix' do
|
||||
expect(helper.avatar_icon(user.email).to_s).
|
||||
to eq("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
helper.avatar_icon('foo@example.com', 20, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an url for the avatar with relative url' do
|
||||
stub_config_setting(relative_url_root: '/gitlab')
|
||||
# Must be stubbed after the stub above, and separately
|
||||
stub_config_setting(url: Settings.send(:build_gitlab_url))
|
||||
describe 'using a user' do
|
||||
context 'when only_path is true' do
|
||||
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
|
||||
|
||||
user = create(:user, avatar: File.open(uploaded_image_temp_path))
|
||||
|
||||
expect(helper.avatar_icon(user.email).to_s).
|
||||
to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
|
||||
end
|
||||
|
||||
it 'calls gravatar_icon when no User exists with the given email' do
|
||||
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
|
||||
|
||||
helper.avatar_icon('foo@example.com', 20, 2)
|
||||
end
|
||||
|
||||
describe 'using a User' do
|
||||
it 'returns an URL for the avatar' do
|
||||
user = create(:user, avatar: File.open(uploaded_image_temp_path))
|
||||
|
||||
expect(helper.avatar_icon(user).to_s).
|
||||
to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif")
|
||||
context 'when only_path is false' do
|
||||
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
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
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', () => {
|
||||
const jsonFixtureName = 'pipelines/pipelines.json';
|
||||
let pipeline;
|
||||
let PipelinesTable;
|
||||
|
||||
preloadFixtures('static/pipelines_table.html.raw');
|
||||
preloadFixtures(jsonFixtureName);
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/pipelines_table.html.raw');
|
||||
PipelinesTable = Vue.extend(pipelinesTable);
|
||||
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
|
||||
pipeline = pipelines.find(p => p.id === 1);
|
||||
});
|
||||
|
@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
Vue.http.interceptors.push(pipelinesEmptyResponse);
|
||||
|
||||
this.component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
propsData: {
|
||||
endpoint: 'endpoint',
|
||||
helpPagePath: 'foo',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
Vue.http.interceptors.push(pipelinesResponse);
|
||||
|
||||
this.component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
propsData: {
|
||||
endpoint: 'endpoint',
|
||||
helpPagePath: 'foo',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
Vue.http.interceptors.push(pipelinesErrorResponse);
|
||||
|
||||
this.component = new PipelinesTable({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
propsData: {
|
||||
endpoint: 'endpoint',
|
||||
helpPagePath: 'foo',
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
|
|
@ -55,13 +55,20 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
|
|||
render_merge_request(example.description, merge_request)
|
||||
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
|
||||
|
||||
def render_merge_request(fixture_file_name, merge_request)
|
||||
get :show,
|
||||
def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html)
|
||||
get action,
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: merge_request.to_param
|
||||
id: merge_request.to_param,
|
||||
format: format
|
||||
|
||||
expect(response).to be_success
|
||||
store_frontend_fixture(response, fixture_file_name)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } }
|
|
@ -95,6 +95,18 @@ describe('Description component', () => {
|
|||
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) => {
|
||||
|
|
|
@ -7,54 +7,92 @@ import '~/render_gfm';
|
|||
import '~/render_math';
|
||||
import '~/notes';
|
||||
|
||||
const upArrowKeyCode = 38;
|
||||
|
||||
describe('Merge request notes', () => {
|
||||
window.gon = window.gon || {};
|
||||
window.gl = window.gl || {};
|
||||
gl.utils = gl.utils || {};
|
||||
|
||||
const fixture = 'merge_requests/diff_comment.html.raw';
|
||||
preloadFixtures(fixture);
|
||||
const discussionTabFixture = 'merge_requests/diff_comment.html.raw';
|
||||
const changesTabJsonFixture = 'merge_requests/changes_tab_with_comments.json';
|
||||
preloadFixtures(discussionTabFixture, changesTabJsonFixture);
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(fixture);
|
||||
gl.utils.disableButtonIfEmptyField = _.noop;
|
||||
window.project_uploads_path = 'http://test.host/uploads';
|
||||
$('body').data('page', 'projects:merge_requests:show');
|
||||
window.gon.current_user_id = $('.note:last').data('author-id');
|
||||
describe('Discussion tab with diff comments', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures(discussionTabFixture);
|
||||
gl.utils.disableButtonIfEmptyField = _.noop;
|
||||
window.project_uploads_path = 'http://test.host/uploads';
|
||||
$('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 when triggered in main form', () => {
|
||||
const upArrowEvent = $.Event('keydown');
|
||||
upArrowEvent.which = 38;
|
||||
|
||||
spyOnEvent('.note:last .js-note-edit', 'click');
|
||||
|
||||
$('.js-note-text').trigger(upArrowEvent);
|
||||
|
||||
expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
|
||||
return new Notes('', []);
|
||||
});
|
||||
|
||||
it('edits last comment in discussion when triggered in discussion form', (done) => {
|
||||
const upArrowEvent = $.Event('keydown');
|
||||
upArrowEvent.which = 38;
|
||||
describe('up arrow', () => {
|
||||
it('edits last comment when triggered in main form', () => {
|
||||
const upArrowEvent = $.Event('keydown');
|
||||
upArrowEvent.which = upArrowKeyCode;
|
||||
|
||||
spyOnEvent('.note-discussion .js-note-edit', 'click');
|
||||
spyOnEvent('.note:last .js-note-edit', 'click');
|
||||
|
||||
$('.js-discussion-reply-button').click();
|
||||
$('.js-note-text').trigger(upArrowEvent);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
$('.note-discussion .js-note-text'),
|
||||
).toExist();
|
||||
expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
|
||||
});
|
||||
|
||||
$('.note-discussion .js-note-text').trigger(upArrowEvent);
|
||||
it('edits last comment in discussion when triggered in discussion form', (done) => {
|
||||
const upArrowEvent = $.Event('keydown');
|
||||
upArrowEvent.which = upArrowKeyCode;
|
||||
|
||||
expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
|
||||
spyOnEvent('.note-discussion .js-note-edit', 'click');
|
||||
|
||||
done();
|
||||
$('.js-discussion-reply-button').click();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
$('.note-discussion .js-note-text'),
|
||||
).toExist();
|
||||
|
||||
$('.note-discussion .js-note-text').trigger(upArrowEvent);
|
||||
|
||||
expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import asyncButtonComp from '~/pipelines/components/async_button.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
|
||||
describe('Pipelines Async Button', () => {
|
||||
let component;
|
||||
let spy;
|
||||
let AsyncButtonComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
AsyncButtonComponent = Vue.extend(asyncButtonComp);
|
||||
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
|
||||
|
||||
component = new AsyncButtonComponent({
|
||||
propsData: {
|
||||
endpoint: '/foo',
|
||||
title: 'Foo',
|
||||
icon: 'fa fa-foo',
|
||||
cssClass: 'bar',
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
|
@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => {
|
|||
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', () => {
|
||||
it('should call the service when confimation is positive', () => {
|
||||
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({
|
||||
propsData: {
|
||||
|
@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => {
|
|||
title: 'Foo',
|
||||
icon: 'fa fa-foo',
|
||||
cssClass: 'bar',
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
confirmActionMessage: 'bar',
|
||||
},
|
||||
}).$mount();
|
||||
|
||||
component.$el.click();
|
||||
expect(spy).toHaveBeenCalledWith('/foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@ import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue';
|
|||
|
||||
describe('Pipelines Actions dropdown', () => {
|
||||
let component;
|
||||
let spy;
|
||||
let actions;
|
||||
let ActionsComponent;
|
||||
|
||||
|
@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => {
|
|||
},
|
||||
];
|
||||
|
||||
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
|
||||
|
||||
component = new ActionsComponent({
|
||||
propsData: {
|
||||
actions,
|
||||
service: {
|
||||
postAction: spy,
|
||||
},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => {
|
|||
).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', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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', () => {
|
||||
const jsonFixtureName = 'pipelines/pipelines.json';
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
describe('Pipelines Table', () => {
|
||||
|
@ -22,7 +22,6 @@ describe('Pipelines Table', () => {
|
|||
component = new PipelinesTableComponent({
|
||||
propsData: {
|
||||
pipelines: [],
|
||||
service: {},
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
|
@ -48,7 +47,6 @@ describe('Pipelines Table', () => {
|
|||
const component = new PipelinesTableComponent({
|
||||
propsData: {
|
||||
pipelines: [],
|
||||
service: {},
|
||||
},
|
||||
}).$mount();
|
||||
expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
|
||||
|
@ -58,10 +56,8 @@ describe('Pipelines Table', () => {
|
|||
describe('with data', () => {
|
||||
it('should render rows', () => {
|
||||
const component = new PipelinesTableComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
pipelines: [pipeline],
|
||||
service: {},
|
||||
},
|
||||
}).$mount();
|
||||
|
|
@ -208,5 +208,9 @@ describe Gitlab::Email::ReplyParser, lib: true 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")
|
||||
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
|
||||
|
|
|
@ -15,6 +15,36 @@ describe Gitlab::Metrics do
|
|||
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
|
||||
it 'returns a boolean' do
|
||||
expect(described_class.prometheus_metrics_enabled?).to be_in([true, false])
|
||||
|
|
|
@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do
|
|||
expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
|
||||
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
|
||||
|
|
|
@ -4,6 +4,18 @@ describe ProjectFeature do
|
|||
let(:project) { create(:empty_project) }
|
||||
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
|
||||
let(:features) { %w(issues wiki builds merge_requests snippets repository) }
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do
|
|||
def build_message(status_text = status, name = user[:name])
|
||||
"<http://example.gitlab.com|project_name>:" \
|
||||
" 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"
|
||||
end
|
||||
end
|
||||
|
@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do
|
|||
expect(subject.pretext).to be_empty
|
||||
expect(subject.attachments).to eq(message)
|
||||
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)',
|
||||
text: 'in 02:00:10',
|
||||
image: ''
|
||||
|
@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do
|
|||
expect(subject.pretext).to be_empty
|
||||
expect(subject.attachments).to eq(message)
|
||||
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)',
|
||||
text: 'in 02:00:10',
|
||||
image: ''
|
||||
|
@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do
|
|||
expect(subject.pretext).to be_empty
|
||||
expect(subject.attachments).to eq(message)
|
||||
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)',
|
||||
text: 'in 02:00:10',
|
||||
image: ''
|
||||
|
@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do
|
|||
def build_markdown_message(status_text = status, name = user[:name])
|
||||
"[project_name](http://example.gitlab.com):" \
|
||||
" 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"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
context 'without markdown' do
|
||||
it 'returns a message regarding pushes' do
|
||||
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>)')
|
||||
expect(subject.attachments).to eq([{
|
||||
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
|
||||
|
@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
|
||||
it 'returns a message regarding pushes' do
|
||||
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(
|
||||
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2")
|
||||
expect(subject.activity).to eq({
|
||||
|
@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
context 'without markdown' do
|
||||
it 'returns a message regarding pushes' do
|
||||
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>')
|
||||
expect(subject.attachments).to be_empty
|
||||
end
|
||||
|
@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
|
||||
it 'returns a message regarding pushes' do
|
||||
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.activity).to eq({
|
||||
title: 'test.user created tag',
|
||||
|
@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
context 'without markdown' do
|
||||
it 'returns a message regarding a new branch' do
|
||||
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>')
|
||||
expect(subject.attachments).to be_empty
|
||||
end
|
||||
|
@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
|
||||
it 'returns a message regarding a new branch' do
|
||||
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.activity).to eq({
|
||||
title: 'test.user created branch',
|
||||
|
@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
context 'without markdown' do
|
||||
it 'returns a message regarding a removed branch' do
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do
|
|||
|
||||
it 'returns a message regarding a removed branch' do
|
||||
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.activity).to eq({
|
||||
title: 'test.user removed branch',
|
||||
|
|
|
@ -2060,4 +2060,36 @@ describe Project, models: true do
|
|||
expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i)
|
||||
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
|
||||
|
|
|
@ -11,7 +11,7 @@ describe API::Users do
|
|||
let(:not_existing_user_id) { (User.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
|
||||
it "returns authentication error" do
|
||||
get api("/users")
|
||||
|
@ -76,6 +76,12 @@ describe API::Users do
|
|||
|
||||
expect(response).to have_http_status(403)
|
||||
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
|
||||
|
||||
context "when admin" 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 'last_sign_in_at'
|
||||
expect(json_response.first.keys).to include 'confirmed_at'
|
||||
expect(json_response.first.keys).to include 'is_admin'
|
||||
end
|
||||
|
||||
it "returns an array of external users" do
|
||||
|
|
|
@ -7,6 +7,38 @@ describe API::V3::Users do
|
|||
let(:email) { create(:email, user: user) }
|
||||
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
|
||||
before { admin }
|
||||
|
||||
|
|
|
@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do
|
|||
it { expect(project.namespace).to eq(group) }
|
||||
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
|
||||
before do
|
||||
@result = transfer_project(project, user, nil)
|
||||
|
|
|
@ -13,9 +13,7 @@ shared_examples 'reportable note' do
|
|||
|
||||
it 'dropdown has Edit, Report and Delete links' do
|
||||
dropdown = comment.find(more_actions_selector)
|
||||
|
||||
dropdown.click
|
||||
dropdown.find('.dropdown-menu li', match: :first)
|
||||
open_dropdown(dropdown)
|
||||
|
||||
expect(dropdown).to have_button('Edit comment')
|
||||
expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
|
||||
|
@ -24,13 +22,16 @@ shared_examples 'reportable note' do
|
|||
|
||||
it 'Report button links to a report page' do
|
||||
dropdown = comment.find(more_actions_selector)
|
||||
|
||||
dropdown.click
|
||||
dropdown.find('.dropdown-menu li', match: :first)
|
||||
open_dropdown(dropdown)
|
||||
|
||||
dropdown.click_link('Report as abuse')
|
||||
|
||||
expect(find('#user_name')['value']).to match(note.author.username)
|
||||
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
|
||||
end
|
||||
|
||||
def open_dropdown(dropdown)
|
||||
dropdown.click
|
||||
dropdown.find('.dropdown-menu li', match: :first)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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
|
Loading…
Reference in New Issue