Merge branch 'master' into auto-search-when-state-changed
This commit is contained in:
commit
b6bded14be
|
@ -1 +1 @@
|
|||
0.4.2
|
||||
0.4.3
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* global ActiveTabMemoizer */
|
||||
/* global ShortcutsNavigation */
|
||||
/* global Build */
|
||||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
/* global ShortcutsIssuable */
|
||||
/* global ZenMode */
|
||||
/* global Milestone */
|
||||
|
@ -127,10 +127,9 @@ import ShortcutsBlob from './shortcuts_blob';
|
|||
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
Issuable.init();
|
||||
new gl.IssuableBulkActions({
|
||||
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
|
||||
});
|
||||
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
|
||||
IssuableIndex.init(pagePrefix);
|
||||
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new UsersSelect();
|
||||
break;
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
</span>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="action in actions">
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
@ -421,14 +421,19 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<tr :class="{ 'js-child-row': model.isChildren }">
|
||||
<td>
|
||||
<div
|
||||
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
|
||||
<div class="table-section section-10" role="gridcell">
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="table-mobile-header">
|
||||
Environment
|
||||
</div>
|
||||
<a
|
||||
v-if="!model.isFolder"
|
||||
class="environment-name"
|
||||
:class="{ 'prepend-left-default': model.isChildren }"
|
||||
class="environment-name flex-truncate-parent table-mobile-content"
|
||||
:href="environmentPath">
|
||||
{{model.name}}
|
||||
<span class="flex-truncate-child">{{model.name}}</span>
|
||||
</a>
|
||||
<span
|
||||
v-else
|
||||
|
@ -461,9 +466,9 @@ export default {
|
|||
{{model.size}}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</div>
|
||||
|
||||
<td class="deployment-column">
|
||||
<div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
|
||||
<span v-if="shouldRenderDeploymentID">
|
||||
{{deploymentInternalId}}
|
||||
</span>
|
||||
|
@ -478,21 +483,26 @@ export default {
|
|||
:tooltip-text="deploymentUser.username"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
</div>
|
||||
|
||||
<td class="environments-build-cell">
|
||||
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
|
||||
<a
|
||||
v-if="shouldRenderBuildName"
|
||||
class="build-link"
|
||||
:href="buildPath">
|
||||
{{buildName}}
|
||||
</a>
|
||||
</td>
|
||||
</div>
|
||||
|
||||
<td>
|
||||
<div class="table-section section-25" role="gridcell">
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="table-mobile-header">
|
||||
Commit
|
||||
</div>
|
||||
<div
|
||||
v-if="!model.isFolder && hasLastDeploymentKey"
|
||||
class="js-commit-component">
|
||||
class="js-commit-component table-mobile-content">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:commit-ref="commitRef"
|
||||
|
@ -501,25 +511,30 @@ export default {
|
|||
:title="commitTitle"
|
||||
:author="commitAuthor"/>
|
||||
</div>
|
||||
<p
|
||||
<div
|
||||
v-if="!model.isFolder && !hasLastDeploymentKey"
|
||||
class="commit-title">
|
||||
No deployments yet
|
||||
</p>
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<td>
|
||||
<span
|
||||
v-if="!model.isFolder && canShowDate"
|
||||
class="environment-created-date-timeago">
|
||||
{{createdDate}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="environments-actions">
|
||||
<div class="table-section section-10" role="gridcell">
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="btn-group pull-right"
|
||||
class="table-mobile-header">
|
||||
Updated
|
||||
</div>
|
||||
<span
|
||||
v-if="!model.isFolder && canShowDate"
|
||||
class="environment-created-date-timeago table-mobile-content">
|
||||
{{createdDate}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
|
||||
<div
|
||||
v-if="!model.isFolder"
|
||||
class="btn-group environment-action-buttons"
|
||||
role="group">
|
||||
|
||||
<actions-component
|
||||
|
@ -553,6 +568,6 @@ export default {
|
|||
:retry-url="retryUrl"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -19,7 +19,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<a
|
||||
class="btn monitoring-url has-tooltip"
|
||||
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
|
||||
data-container="body"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:href="monitoringUrl"
|
||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class="btn hidden-xs hidden-sm"
|
||||
@click="onClick"
|
||||
:disabled="isLoading">
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn stop-env-link has-tooltip"
|
||||
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
|
||||
data-container="body"
|
||||
@click="onClick"
|
||||
:disabled="isLoading"
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<a
|
||||
class="btn terminal-button has-tooltip"
|
||||
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
|
||||
data-container="body"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
|
|
|
@ -45,32 +45,28 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<table class="table ci-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="environments-name">
|
||||
<div class="ci-table" role="grid">
|
||||
<div class="gl-responsive-table-row table-row-header" role="row">
|
||||
<div class="table-section section-10 environments-name" role="rowheader">
|
||||
Environment
|
||||
</th>
|
||||
<th class="environments-deploy">
|
||||
Last deployment
|
||||
</th>
|
||||
<th class="environments-build">
|
||||
</div>
|
||||
<div class="table-section section-10 environments-deploy" role="rowheader">
|
||||
Deployment
|
||||
</div>
|
||||
<div class="table-section section-15 environments-build" role="rowheader">
|
||||
Job
|
||||
</th>
|
||||
<th class="environments-commit">
|
||||
</div>
|
||||
<div class="table-section section-25 environments-commit" role="rowheader">
|
||||
Commit
|
||||
</th>
|
||||
<th class="environments-date">
|
||||
</div>
|
||||
<div class="table-section section-10 environments-date" role="rowheader">
|
||||
Updated
|
||||
</th>
|
||||
<th class="environments-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-for="model in environments"
|
||||
v-bind:model="model">
|
||||
<tr
|
||||
<div
|
||||
is="environment-item"
|
||||
:model="model"
|
||||
:can-create-deployment="canCreateDeployment"
|
||||
|
@ -78,14 +74,12 @@ export default {
|
|||
/>
|
||||
|
||||
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
|
||||
<tr v-if="isLoadingFolderContent">
|
||||
<td colspan="6">
|
||||
<div v-if="isLoadingFolderContent">
|
||||
<loading-icon size="2" />
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<tr
|
||||
<div
|
||||
is="environment-item"
|
||||
v-for="children in model.children"
|
||||
:model="children"
|
||||
|
@ -93,20 +87,17 @@ export default {
|
|||
:can-read-environment="canReadEnvironment"
|
||||
/>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
colspan="6"
|
||||
class="text-center">
|
||||
<div>
|
||||
<div class="text-center prepend-top-10">
|
||||
<a
|
||||
:href="folderUrl(model)"
|
||||
class="btn btn-default">
|
||||
Show all
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
|
||||
/* global IssuableIndex */
|
||||
/* global Flash */
|
||||
|
||||
export default {
|
||||
init({ container, form, issues, prefixId } = {}) {
|
||||
this.prefixId = prefixId || 'issue_';
|
||||
this.form = form || this.getElement('.bulk-update');
|
||||
this.$labelDropdown = this.form.find('.js-label-select');
|
||||
this.issues = issues || this.getElement('.issues-list .issue');
|
||||
this.willUpdateLabels = false;
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
||||
},
|
||||
|
||||
onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
return this.submit();
|
||||
},
|
||||
|
||||
submit() {
|
||||
const _this = this;
|
||||
const xhr = $.ajax({
|
||||
url: this.form.attr('action'),
|
||||
method: this.form.attr('method'),
|
||||
dataType: 'JSON',
|
||||
data: this.getFormDataAsObject()
|
||||
});
|
||||
xhr.done(() => window.location.reload());
|
||||
xhr.fail(() => this.onFormSubmitFailure());
|
||||
},
|
||||
|
||||
onFormSubmitFailure() {
|
||||
this.form.find('[type="submit"]').enable();
|
||||
return new Flash("Issue update failed");
|
||||
},
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.issues.has('.selected_issue:checked');
|
||||
},
|
||||
|
||||
getLabelsFromSelection() {
|
||||
const labels = [];
|
||||
this.getSelectedIssues().map(function() {
|
||||
const labelsData = $(this).data('labels');
|
||||
if (labelsData) {
|
||||
return labelsData.map(function(labelId) {
|
||||
if (labels.indexOf(labelId) === -1) {
|
||||
return labels.push(labelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Will return only labels that were marked previously and the user has unmarked
|
||||
* @return {Array} Label IDs
|
||||
*/
|
||||
|
||||
getUnmarkedIndeterminedLabels() {
|
||||
const result = [];
|
||||
const labelsToKeep = this.$labelDropdown.data('indeterminate');
|
||||
|
||||
this.getLabelsFromSelection().forEach((id) => {
|
||||
if (labelsToKeep.indexOf(id) === -1) {
|
||||
result.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Simple form serialization, it will return just what we need
|
||||
* Returns key/value pairs from form data
|
||||
*/
|
||||
|
||||
getFormDataAsObject() {
|
||||
const formData = {
|
||||
update: {
|
||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
||||
// For Merge Requests
|
||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
||||
// For Issues
|
||||
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
|
||||
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
|
||||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
||||
add_label_ids: [],
|
||||
remove_label_ids: []
|
||||
}
|
||||
};
|
||||
if (this.willUpdateLabels) {
|
||||
formData.update.add_label_ids = this.$labelDropdown.data('marked');
|
||||
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
|
||||
}
|
||||
return formData;
|
||||
},
|
||||
|
||||
setOriginalDropdownData() {
|
||||
const $labelSelect = $('.bulk-update .js-label-select');
|
||||
$labelSelect.data('common', this.getOriginalCommonIds());
|
||||
$labelSelect.data('marked', this.getOriginalMarkedIds());
|
||||
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalCommonIds() {
|
||||
const labelIds = [];
|
||||
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalMarkedIds() {
|
||||
const labelIds = [];
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
},
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalIndeterminateIds() {
|
||||
const uniqueIds = [];
|
||||
const labelIds = [];
|
||||
let issuableLabels = [];
|
||||
|
||||
// Collect unique label IDs for all checked issues
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
|
||||
issuableLabels.forEach((labelId) => {
|
||||
// Store unique IDs
|
||||
if (uniqueIds.indexOf(labelId) === -1) {
|
||||
uniqueIds.push(labelId);
|
||||
}
|
||||
});
|
||||
// Store array of IDs per issuable
|
||||
labelIds.push(issuableLabels);
|
||||
});
|
||||
// Add uniqueIds to add it as argument for _.intersection
|
||||
labelIds.unshift(uniqueIds);
|
||||
// Return IDs that are present but not in all selected issueables
|
||||
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
|
||||
},
|
||||
|
||||
getElement(selector) {
|
||||
this.scopeEl = this.scopeEl || $('.content');
|
||||
return this.scopeEl.find(selector);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,165 @@
|
|||
/* eslint-disable class-methods-use-this, no-new */
|
||||
/* global LabelsSelect */
|
||||
/* global MilestoneSelect */
|
||||
/* global IssueStatusSelect */
|
||||
/* global SubscriptionSelect */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
const HIDDEN_CLASS = 'hidden';
|
||||
const DISABLED_CONTENT_CLASS = 'disabled-content';
|
||||
const SIDEBAR_EXPANDED_CLASS = 'right-sidebar-expanded issuable-bulk-update-sidebar';
|
||||
const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-sidebar';
|
||||
|
||||
export default class IssuableBulkUpdateSidebar {
|
||||
constructor() {
|
||||
this.initDomElements();
|
||||
this.bindEvents();
|
||||
this.initDropdowns();
|
||||
this.setupBulkUpdateActions();
|
||||
}
|
||||
|
||||
initDomElements() {
|
||||
this.$page = $('.page-with-sidebar');
|
||||
this.$sidebar = $('.right-sidebar');
|
||||
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
|
||||
this.$bulkEditSubmitBtn = $('.update-selected-issues');
|
||||
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
|
||||
this.$otherFilters = $('.issues-other-filters');
|
||||
this.$checkAllContainer = $('.check-all-holder');
|
||||
this.$issueChecks = $('.issue-check');
|
||||
this.$issuesList = $('.selected_issue');
|
||||
this.$issuableIdsInput = $('#update_issuable_ids');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.$bulkUpdateEnableBtn.on('click', e => this.toggleBulkEdit(e, true));
|
||||
this.$bulkEditCancelBtn.on('click', e => this.toggleBulkEdit(e, false));
|
||||
this.$checkAllContainer.on('click', e => this.selectAll(e));
|
||||
this.$issuesList.on('change', () => this.updateFormState());
|
||||
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
|
||||
this.$checkAllContainer.on('click', () => this.updateFormState());
|
||||
}
|
||||
|
||||
initDropdowns() {
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssueStatusSelect();
|
||||
new SubscriptionSelect();
|
||||
}
|
||||
|
||||
getNavHeight() {
|
||||
const navbarHeight = $('.navbar-gitlab').outerHeight();
|
||||
const layoutNavHeight = $('.layout-nav').outerHeight();
|
||||
const subNavScroll = $('.sub-nav-scroll').outerHeight();
|
||||
return navbarHeight + layoutNavHeight + subNavScroll;
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
if (!this.navHeight) {
|
||||
this.navHeight = this.getNavHeight();
|
||||
}
|
||||
|
||||
if (!this.sidebarInitialized) {
|
||||
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
this.sidebarInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
setupBulkUpdateActions() {
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
||||
updateFormState() {
|
||||
const noCheckedIssues = !$('.selected_issue:checked').length;
|
||||
|
||||
this.toggleSubmitButtonDisabled(noCheckedIssues);
|
||||
this.updateSelectedIssuableIds();
|
||||
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
||||
prepForSubmit() {
|
||||
// if submit button is disabled, submission is blocked. This ensures we disable after
|
||||
// form submission is carried out
|
||||
setTimeout(() => this.$bulkEditSubmitBtn.disable());
|
||||
this.updateSelectedIssuableIds();
|
||||
}
|
||||
|
||||
toggleBulkEdit(e, enable) {
|
||||
e.preventDefault();
|
||||
|
||||
this.toggleSidebarDisplay(enable);
|
||||
this.toggleBulkEditButtonDisabled(enable);
|
||||
this.toggleOtherFiltersDisabled(enable);
|
||||
this.toggleCheckboxDisplay(enable);
|
||||
|
||||
if (enable) {
|
||||
this.initSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedIssuableIds() {
|
||||
this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const checkAllButtonState = this.$checkAllContainer.find('input').prop('checked');
|
||||
|
||||
this.$issuesList.prop('checked', checkAllButtonState);
|
||||
}
|
||||
|
||||
toggleSidebarDisplay(show) {
|
||||
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
|
||||
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
|
||||
}
|
||||
|
||||
toggleBulkEditButtonDisabled(disable) {
|
||||
if (disable) {
|
||||
this.$bulkUpdateEnableBtn.disable();
|
||||
} else {
|
||||
this.$bulkUpdateEnableBtn.enable();
|
||||
}
|
||||
}
|
||||
|
||||
toggleCheckboxDisplay(show) {
|
||||
this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
|
||||
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
|
||||
}
|
||||
|
||||
toggleOtherFiltersDisabled(disable) {
|
||||
this.$otherFilters.toggleClass(DISABLED_CONTENT_CLASS, disable);
|
||||
}
|
||||
|
||||
toggleSubmitButtonDisabled(disable) {
|
||||
if (disable) {
|
||||
this.$bulkEditSubmitBtn.disable();
|
||||
} else {
|
||||
this.$bulkEditSubmitBtn.enable();
|
||||
}
|
||||
}
|
||||
// loosely based on method of the same name in right_sidebar.js
|
||||
setSidebarHeight() {
|
||||
const currentScrollDepth = window.pageYOffset || 0;
|
||||
const diff = this.navHeight - currentScrollDepth;
|
||||
|
||||
if (diff > 0) {
|
||||
this.$sidebar.outerHeight(window.innerHeight - diff);
|
||||
} else {
|
||||
this.$sidebar.outerHeight('100%');
|
||||
}
|
||||
}
|
||||
|
||||
static getCheckedIssueIds() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
|
||||
if ($checkedIssues.length > 0) {
|
||||
return $.map($checkedIssues, value => $(value).data('id'));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -1,30 +1,33 @@
|
|||
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
|
||||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
|
||||
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
((global) => {
|
||||
var issuable_created;
|
||||
|
||||
issuable_created = false;
|
||||
|
||||
global.Issuable = {
|
||||
init: function() {
|
||||
Issuable.initTemplates();
|
||||
Issuable.initSearch();
|
||||
Issuable.initChecks();
|
||||
Issuable.initResetFilters();
|
||||
Issuable.resetIncomingEmailToken();
|
||||
return Issuable.initLabelFilterRemove();
|
||||
global.IssuableIndex = {
|
||||
init: function(pagePrefix) {
|
||||
IssuableIndex.initTemplates();
|
||||
IssuableIndex.initSearch();
|
||||
IssuableIndex.initBulkUpdate(pagePrefix);
|
||||
IssuableIndex.initResetFilters();
|
||||
IssuableIndex.resetIncomingEmailToken();
|
||||
IssuableIndex.initLabelFilterRemove();
|
||||
},
|
||||
initTemplates: function() {
|
||||
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
|
||||
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
|
||||
},
|
||||
initSearch: function() {
|
||||
const $searchInput = $('#issuable_search');
|
||||
|
||||
Issuable.initSearchState($searchInput);
|
||||
IssuableIndex.initSearchState($searchInput);
|
||||
|
||||
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
|
||||
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
|
||||
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
|
||||
|
||||
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
|
||||
|
||||
|
@ -37,16 +40,16 @@
|
|||
initSearchState: function($searchInput) {
|
||||
const currentSearchVal = $searchInput.val();
|
||||
|
||||
Issuable.searchState = {
|
||||
IssuableIndex.searchState = {
|
||||
elem: $searchInput,
|
||||
current: currentSearchVal
|
||||
};
|
||||
|
||||
Issuable.maybeFocusOnSearch();
|
||||
IssuableIndex.maybeFocusOnSearch();
|
||||
},
|
||||
accessSearchPristine: function(set) {
|
||||
// store reference to previous value to prevent search on non-mutating keyup
|
||||
const state = Issuable.searchState;
|
||||
const state = IssuableIndex.searchState;
|
||||
const currentSearchVal = state.elem.val();
|
||||
|
||||
if (set) {
|
||||
|
@ -56,10 +59,10 @@
|
|||
}
|
||||
},
|
||||
maybeFocusOnSearch: function() {
|
||||
const currentSearchVal = Issuable.searchState.current;
|
||||
const currentSearchVal = IssuableIndex.searchState.current;
|
||||
if (currentSearchVal && currentSearchVal !== '') {
|
||||
const queryLength = currentSearchVal.length;
|
||||
const $searchInput = Issuable.searchState.elem;
|
||||
const $searchInput = IssuableIndex.searchState.elem;
|
||||
|
||||
/* The following ensures that the cursor is initially placed at
|
||||
* the end of search input when focus is applied. It accounts
|
||||
|
@ -80,7 +83,7 @@
|
|||
const $searchValue = $search.val();
|
||||
const $filtersForm = $('.js-filter-form');
|
||||
const $input = $(`input[name='${$searchName}']`, $filtersForm);
|
||||
const isPristine = Issuable.accessSearchPristine();
|
||||
const isPristine = IssuableIndex.accessSearchPristine();
|
||||
|
||||
if (isPristine) {
|
||||
return;
|
||||
|
@ -92,7 +95,7 @@
|
|||
$input.val($searchValue);
|
||||
}
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
},
|
||||
initLabelFilterRemove: function() {
|
||||
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
|
||||
|
@ -103,7 +106,7 @@
|
|||
return this.value === $button.data('label');
|
||||
}).remove();
|
||||
// Submit the form to get new data
|
||||
Issuable.filterResults($('.filter-form'));
|
||||
IssuableIndex.filterResults($('.filter-form'));
|
||||
});
|
||||
},
|
||||
filterResults: (function(_this) {
|
||||
|
@ -132,38 +135,18 @@
|
|||
gl.utils.visitUrl(baseIssuesUrl);
|
||||
});
|
||||
},
|
||||
initChecks: function() {
|
||||
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
$('.check_all_issues').off('click').on('click', function() {
|
||||
$('.selected_issue').prop('checked', this.checked);
|
||||
return Issuable.checkChanged();
|
||||
});
|
||||
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
|
||||
},
|
||||
checkChanged: function() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
const $updateIssuesIds = $('#update_issuable_ids');
|
||||
const $issuesOtherFilters = $('.issues-other-filters');
|
||||
const $issuesBulkUpdate = $('.issues_bulk_update');
|
||||
initBulkUpdate: function(pagePrefix) {
|
||||
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
|
||||
const alreadyInitialized = !!this.bulkUpdateSidebar;
|
||||
|
||||
this.issuableBulkActions.willUpdateLabels = false;
|
||||
this.issuableBulkActions.setOriginalDropdownData();
|
||||
|
||||
if ($checkedIssues.length > 0) {
|
||||
const ids = $.map($checkedIssues, function(value) {
|
||||
return $(value).data('id');
|
||||
if (userCanBulkUpdate && !alreadyInitialized) {
|
||||
IssuableBulkUpdateActions.init({
|
||||
prefixId: pagePrefix,
|
||||
});
|
||||
$updateIssuesIds.val(ids);
|
||||
$issuesOtherFilters.hide();
|
||||
$issuesBulkUpdate.show();
|
||||
} else {
|
||||
$updateIssuesIds.val([]);
|
||||
$issuesBulkUpdate.hide();
|
||||
$issuesOtherFilters.show();
|
||||
|
||||
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
resetIncomingEmailToken: function() {
|
||||
$('.incoming-email-token-reset').on('click', function(e) {
|
||||
e.preventDefault();
|
|
@ -1,166 +0,0 @@
|
|||
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
|
||||
/* global Issuable */
|
||||
/* global Flash */
|
||||
|
||||
((global) => {
|
||||
class IssuableBulkActions {
|
||||
constructor({ container, form, issues, prefixId } = {}) {
|
||||
this.prefixId = prefixId || 'issue_';
|
||||
this.form = form || this.getElement('.bulk-update');
|
||||
this.$labelDropdown = this.form.find('.js-label-select');
|
||||
this.issues = issues || this.getElement('.issues-list .issue');
|
||||
this.form.data('bulkActions', this);
|
||||
this.willUpdateLabels = false;
|
||||
this.bindEvents();
|
||||
// Fixes bulk-assign not working when navigating through pages
|
||||
Issuable.initChecks();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
||||
}
|
||||
|
||||
onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
return this.submit();
|
||||
}
|
||||
|
||||
submit() {
|
||||
const _this = this;
|
||||
const xhr = $.ajax({
|
||||
url: this.form.attr('action'),
|
||||
method: this.form.attr('method'),
|
||||
dataType: 'JSON',
|
||||
data: this.getFormDataAsObject()
|
||||
});
|
||||
xhr.done(() => window.location.reload());
|
||||
xhr.fail(() => new Flash("Issue update failed"));
|
||||
return xhr.always(this.onFormSubmitAlways.bind(this));
|
||||
}
|
||||
|
||||
onFormSubmitAlways() {
|
||||
return this.form.find('[type="submit"]').enable();
|
||||
}
|
||||
|
||||
getSelectedIssues() {
|
||||
return this.issues.has('.selected_issue:checked');
|
||||
}
|
||||
|
||||
getLabelsFromSelection() {
|
||||
const labels = [];
|
||||
this.getSelectedIssues().map(function() {
|
||||
const labelsData = $(this).data('labels');
|
||||
if (labelsData) {
|
||||
return labelsData.map(function(labelId) {
|
||||
if (labels.indexOf(labelId) === -1) {
|
||||
return labels.push(labelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return only labels that were marked previously and the user has unmarked
|
||||
* @return {Array} Label IDs
|
||||
*/
|
||||
|
||||
getUnmarkedIndeterminedLabels() {
|
||||
const result = [];
|
||||
const labelsToKeep = this.$labelDropdown.data('indeterminate');
|
||||
|
||||
this.getLabelsFromSelection().forEach((id) => {
|
||||
if (labelsToKeep.indexOf(id) === -1) {
|
||||
result.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple form serialization, it will return just what we need
|
||||
* Returns key/value pairs from form data
|
||||
*/
|
||||
|
||||
getFormDataAsObject() {
|
||||
const formData = {
|
||||
update: {
|
||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
||||
// For Merge Requests
|
||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
||||
// For Issues
|
||||
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
|
||||
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
|
||||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
||||
add_label_ids: [],
|
||||
remove_label_ids: []
|
||||
}
|
||||
};
|
||||
if (this.willUpdateLabels) {
|
||||
formData.update.add_label_ids = this.$labelDropdown.data('marked');
|
||||
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
setOriginalDropdownData() {
|
||||
const $labelSelect = $('.bulk-update .js-label-select');
|
||||
$labelSelect.data('common', this.getOriginalCommonIds());
|
||||
$labelSelect.data('marked', this.getOriginalMarkedIds());
|
||||
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalCommonIds() {
|
||||
const labelIds = [];
|
||||
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalMarkedIds() {
|
||||
const labelIds = [];
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
||||
});
|
||||
return _.intersection.apply(this, labelIds);
|
||||
}
|
||||
|
||||
// From issuable's initial bulk selection
|
||||
getOriginalIndeterminateIds() {
|
||||
const uniqueIds = [];
|
||||
const labelIds = [];
|
||||
let issuableLabels = [];
|
||||
|
||||
// Collect unique label IDs for all checked issues
|
||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
||||
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
|
||||
issuableLabels.forEach((labelId) => {
|
||||
// Store unique IDs
|
||||
if (uniqueIds.indexOf(labelId) === -1) {
|
||||
uniqueIds.push(labelId);
|
||||
}
|
||||
});
|
||||
// Store array of IDs per issuable
|
||||
labelIds.push(issuableLabels);
|
||||
});
|
||||
// Add uniqueIds to add it as argument for _.intersection
|
||||
labelIds.unshift(uniqueIds);
|
||||
// Return IDs that are present but not in all selected issueables
|
||||
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
|
||||
}
|
||||
|
||||
getElement(selector) {
|
||||
this.scopeEl = this.scopeEl || $('.content');
|
||||
return this.scopeEl.find(selector);
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableBulkActions = IssuableBulkActions;
|
||||
})(window.gl || (window.gl = {}));
|
|
@ -2,6 +2,8 @@
|
|||
/* global Issuable */
|
||||
/* global ListLabel */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
|
||||
(function() {
|
||||
this.LabelsSelect = (function() {
|
||||
function LabelsSelect(els) {
|
||||
|
@ -430,20 +432,15 @@
|
|||
if ($('.selected_issue:checked').length) {
|
||||
return;
|
||||
}
|
||||
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
|
||||
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
|
||||
};
|
||||
|
||||
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
|
||||
var issuableBulkActions;
|
||||
if ($('.selected_issue:checked').length) {
|
||||
issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
return issuableBulkActions.willUpdateLabels = true;
|
||||
}
|
||||
IssuableBulkUpdateActions.willUpdateLabels = true;
|
||||
};
|
||||
|
||||
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
|
||||
var i, markedIds, unmarkedIds, indeterminateIds;
|
||||
var issuableBulkActions = $('.bulk-update').data('bulkActions');
|
||||
|
||||
markedIds = $dropdown.data('marked') || [];
|
||||
unmarkedIds = $dropdown.data('unmarked') || [];
|
||||
|
@ -469,13 +466,13 @@
|
|||
}
|
||||
|
||||
// If an indeterminate item is being unmarked
|
||||
if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
|
||||
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
|
||||
unmarkedIds.push(value);
|
||||
}
|
||||
|
||||
// If a marked item is being unmarked
|
||||
// (a marked item could also be a label that is present in all selection)
|
||||
if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
|
||||
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
|
||||
unmarkedIds.push(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,12 +104,11 @@ import './group_label_subscription';
|
|||
import './groups_select';
|
||||
import './header';
|
||||
import './importer_status';
|
||||
import './issuable';
|
||||
import './issuable_index';
|
||||
import './issuable_context';
|
||||
import './issuable_form';
|
||||
import './issue';
|
||||
import './issue_status_select';
|
||||
import './issues_bulk_assignment';
|
||||
import './label_manager';
|
||||
import './labels';
|
||||
import './labels_select';
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
|
||||
|
||||
function highlightChanges($elm) {
|
||||
$elm.addClass('highlight-changes');
|
||||
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
|
||||
}
|
||||
|
||||
(function() {
|
||||
this.ProjectNew = (function() {
|
||||
function ProjectNew() {
|
||||
this.toggleSettings = this.toggleSettings.bind(this);
|
||||
this.$selects = $('.features select');
|
||||
this.$repoSelects = this.$selects.filter('.js-repo-select');
|
||||
this.$projectSelects = this.$selects.not('.js-repo-select');
|
||||
|
||||
$('.project-edit-container').on('ajax:before', (function(_this) {
|
||||
return function() {
|
||||
|
@ -26,6 +32,42 @@
|
|||
if (!visibilityContainer) return;
|
||||
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
|
||||
visibilitySelect.init();
|
||||
|
||||
const $visibilitySelect = $(visibilityContainer).find('select');
|
||||
let projectVisibility = $visibilitySelect.val();
|
||||
const PROJECT_VISIBILITY_PRIVATE = '0';
|
||||
|
||||
$visibilitySelect.on('change', () => {
|
||||
const newProjectVisibility = $visibilitySelect.val();
|
||||
|
||||
if (projectVisibility !== newProjectVisibility) {
|
||||
this.$projectSelects.each((idx, select) => {
|
||||
const $select = $(select);
|
||||
const $options = $select.find('option');
|
||||
const values = $.map($options, e => e.value);
|
||||
|
||||
// if switched to "private", limit visibility options
|
||||
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
|
||||
if ($select.val() !== values[0] && $select.val() !== values[1]) {
|
||||
$select.val(values[1]).trigger('change');
|
||||
highlightChanges($select);
|
||||
}
|
||||
$options.slice(2).disable();
|
||||
}
|
||||
|
||||
// if switched from "private", increase visibility for non-disabled options
|
||||
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
|
||||
$options.enable();
|
||||
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
|
||||
$select.val(values[values.length - 1]).trigger('change');
|
||||
highlightChanges($select);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
projectVisibility = newProjectVisibility;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ProjectNew.prototype.toggleSettings = function() {
|
||||
|
@ -56,8 +98,10 @@
|
|||
|
||||
ProjectNew.prototype.toggleRepoVisibility = function () {
|
||||
var $repoAccessLevel = $('.js-repo-access-level select');
|
||||
var $lfsEnabledOption = $('.js-lfs-enabled select');
|
||||
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
|
||||
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
|
||||
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
|
||||
|
||||
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
|
||||
.nextAll()
|
||||
|
@ -71,29 +115,40 @@
|
|||
var $this = $(this);
|
||||
var repoSelectVal = parseInt($this.val(), 10);
|
||||
|
||||
$this.find('option').show();
|
||||
$this.find('option').enable();
|
||||
|
||||
if (selectedVal < repoSelectVal) {
|
||||
$this.val(selectedVal);
|
||||
if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
|
||||
$this.val(selectedVal).trigger('change');
|
||||
highlightChanges($this);
|
||||
}
|
||||
|
||||
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
|
||||
$this.find("option[value='" + selectedVal + "']").nextAll().disable();
|
||||
});
|
||||
|
||||
if (selectedVal) {
|
||||
this.$repoSelects.removeClass('disabled');
|
||||
|
||||
if ($lfsEnabledOption.length) {
|
||||
$lfsEnabledOption.removeClass('disabled');
|
||||
highlightChanges($lfsEnabledOption);
|
||||
}
|
||||
if (containerRegistry) {
|
||||
containerRegistry.style.display = '';
|
||||
}
|
||||
} else {
|
||||
this.$repoSelects.addClass('disabled');
|
||||
|
||||
if ($lfsEnabledOption.length) {
|
||||
$lfsEnabledOption.val('false').addClass('disabled');
|
||||
highlightChanges($lfsEnabledOption);
|
||||
}
|
||||
if (containerRegistry) {
|
||||
containerRegistry.style.display = 'none';
|
||||
containerRegistryCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
prevSelectedVal = selectedVal;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
|
@ -135,8 +135,8 @@ export default {
|
|||
{{shortSha}}
|
||||
</a>
|
||||
|
||||
<p class="commit-title">
|
||||
<span v-if="title">
|
||||
<div class="commit-title flex-truncate-parent">
|
||||
<span v-if="title" class="flex-truncate-child">
|
||||
<user-avatar-link
|
||||
v-if="hasAuthor"
|
||||
class="avatar-image-container"
|
||||
|
@ -153,7 +153,7 @@ export default {
|
|||
<span v-else>
|
||||
Cant find HEAD commit for this branch
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -49,3 +49,4 @@
|
|||
@import "framework/icons.scss";
|
||||
@import "framework/snippets.scss";
|
||||
@import "framework/memory_graph.scss";
|
||||
@import "framework/responsive-tables.scss";
|
||||
|
|
|
@ -445,3 +445,9 @@ table {
|
|||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-content {
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,6 @@
|
|||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.issues_bulk_update {
|
||||
.dropdown-menu-toggle {
|
||||
width: 132px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-item:not(:last-child) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
@ -376,12 +370,6 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
.issue-bulk-update-dropdown-toggle {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.issues-details-filters {
|
||||
padding: 0 0 10px;
|
||||
|
|
|
@ -29,10 +29,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.issues-holder .issue-check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rss-btn {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
@mixin flex-max-width($max) {
|
||||
flex: 0 0 #{$max + '%'};
|
||||
max-width: #{$max + '%'};
|
||||
}
|
||||
|
||||
.gl-responsive-table-row {
|
||||
margin-top: 10px;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding: 15px 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-bottom: 1px solid $white-normal;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
white-space: nowrap;
|
||||
|
||||
$section-widths: 10 15 25 30;
|
||||
@each $width in $section-widths {
|
||||
&.section-#{$width} {
|
||||
flex: 0 0 #{$width + '%'};
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
max-width: #{$width + '%'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.table-button-footer) {
|
||||
@media (max-width: $screen-sm-max) {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
padding: 10px;
|
||||
align-items: center;
|
||||
height: 62px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid $white-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row-header {
|
||||
font-size: 13px;
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-mobile-header {
|
||||
color: $gl-text-color-secondary;
|
||||
@include flex-max-width(40);
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-mobile-content {
|
||||
@media (max-width: $screen-sm-max) {
|
||||
@include flex-max-width(60);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-truncate-parent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-truncate-child {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
flex: 0 0 90%;
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@
|
|||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@
|
|||
z-index: 300;
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
|
||||
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
@ -88,3 +88,35 @@
|
|||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin maintain-sidebar-dimensions {
|
||||
display: block;
|
||||
width: $gutter-width;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.issues-bulk-update.right-sidebar {
|
||||
@include maintain-sidebar-dimensions;
|
||||
transition: right $sidebar-transition-duration;
|
||||
right: -$gutter-width;
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
@include maintain-sidebar-dimensions;
|
||||
right: -$gutter-width;
|
||||
|
||||
.block {
|
||||
padding: 16px 0;
|
||||
width: 250px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-sidebar {
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
|
|||
$divergence-graph-separator-bg: #ccc;
|
||||
$general-hover-transition-duration: 100ms;
|
||||
$general-hover-transition-curve: linear;
|
||||
$highlight-changes-color: rgb(235, 255, 232);
|
||||
|
||||
|
||||
/*
|
||||
|
|
|
@ -11,34 +11,7 @@
|
|||
}
|
||||
|
||||
.environments-container {
|
||||
.table-holder {
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table.ci-table {
|
||||
.environments-actions {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.environments-commit,
|
||||
.environments-actions {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.environments-date {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.environments-name,
|
||||
.environments-deploy,
|
||||
.environments-build {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.ci-table {
|
||||
.deployment-column {
|
||||
> span {
|
||||
word-break: break-all;
|
||||
|
@ -150,6 +123,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
.gl-responsive-table-row {
|
||||
.environments-actions {
|
||||
@media (min-width: $screen-md-min) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
background-color: $gray-normal;
|
||||
align-self: stretch;
|
||||
border-top: 1px solid $border-color;
|
||||
|
||||
.environment-action-buttons {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
> .btn-group,
|
||||
.external-url {
|
||||
flex: 1;
|
||||
flex-basis: 28px;
|
||||
}
|
||||
|
||||
.dropdown-new {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-row {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid $white-normal;
|
||||
|
||||
@media (max-width: $screen-sm-max) {
|
||||
border-top: 1px solid $white-normal;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.prometheus-graph {
|
||||
text {
|
||||
fill: $gl-text-color;
|
||||
|
|
|
@ -29,6 +29,20 @@
|
|||
& > .form-group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
select option[disabled] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
background: transparent;
|
||||
transition: background 2s ease-out;
|
||||
|
||||
&.highlight-changes {
|
||||
background: $highlight-changes-color;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.help-block {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
class EventsFinder
|
||||
attr_reader :source, :params, :current_user
|
||||
|
||||
# Used to filter Events
|
||||
#
|
||||
# Arguments:
|
||||
# source - which user or project to looks for events on
|
||||
# current_user - only return events for projects visible to this user
|
||||
# params:
|
||||
# action: string
|
||||
# target_type: string
|
||||
# before: datetime
|
||||
# after: datetime
|
||||
#
|
||||
def initialize(params = {})
|
||||
@source = params.delete(:source)
|
||||
@current_user = params.delete(:current_user)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
events = source.events
|
||||
|
||||
events = by_current_user_access(events)
|
||||
events = by_action(events)
|
||||
events = by_target_type(events)
|
||||
events = by_created_at_before(events)
|
||||
events = by_created_at_after(events)
|
||||
|
||||
events
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def by_current_user_access(events)
|
||||
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
|
||||
end
|
||||
|
||||
def by_action(events)
|
||||
return events unless Event::ACTIONS[params[:action]]
|
||||
|
||||
events.where(action: Event::ACTIONS[params[:action]])
|
||||
end
|
||||
|
||||
def by_target_type(events)
|
||||
return events unless Event::TARGET_TYPES[params[:target_type]]
|
||||
|
||||
events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
|
||||
end
|
||||
|
||||
def by_created_at_before(events)
|
||||
return events unless params[:before]
|
||||
|
||||
events.where('events.created_at < ?', params[:before].beginning_of_day)
|
||||
end
|
||||
|
||||
def by_created_at_after(events)
|
||||
return events unless params[:after]
|
||||
|
||||
events.where('events.created_at > ?', params[:after].end_of_day)
|
||||
end
|
||||
end
|
|
@ -138,11 +138,15 @@ module ProjectsHelper
|
|||
|
||||
if @project.private?
|
||||
level = @project.project_feature.send(field)
|
||||
options.delete('Everyone with access')
|
||||
highest_available_option = options.values.max if level == ProjectFeature::ENABLED
|
||||
disabled_option = ProjectFeature::ENABLED
|
||||
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
|
||||
end
|
||||
|
||||
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
|
||||
options = options_for_select(
|
||||
options,
|
||||
selected: highest_available_option || @project.project_feature.public_send(field),
|
||||
disabled: disabled_option
|
||||
)
|
||||
|
||||
content_tag(
|
||||
:select,
|
||||
|
|
|
@ -31,9 +31,9 @@ module VisibilityLevelHelper
|
|||
when Gitlab::VisibilityLevel::PRIVATE
|
||||
"Project access must be granted explicitly to each user."
|
||||
when Gitlab::VisibilityLevel::INTERNAL
|
||||
"The project can be cloned by any logged in user."
|
||||
"The project can be accessed by any logged in user."
|
||||
when Gitlab::VisibilityLevel::PUBLIC
|
||||
"The project can be cloned without any authentication."
|
||||
"The project can be accessed without any authentication."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
|
|||
DESTROYED = 10
|
||||
EXPIRED = 11 # User left project due to expiry
|
||||
|
||||
ACTIONS = HashWithIndifferentAccess.new(
|
||||
created: CREATED,
|
||||
updated: UPDATED,
|
||||
closed: CLOSED,
|
||||
reopened: REOPENED,
|
||||
pushed: PUSHED,
|
||||
commented: COMMENTED,
|
||||
merged: MERGED,
|
||||
joined: JOINED,
|
||||
left: LEFT,
|
||||
destroyed: DESTROYED,
|
||||
expired: EXPIRED
|
||||
).freeze
|
||||
|
||||
TARGET_TYPES = HashWithIndifferentAccess.new(
|
||||
issue: Issue,
|
||||
milestone: Milestone,
|
||||
merge_request: MergeRequest,
|
||||
note: Note,
|
||||
project: Project,
|
||||
snippet: Snippet,
|
||||
user: User
|
||||
).freeze
|
||||
|
||||
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
|
||||
|
||||
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
|
||||
|
@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
|
|||
def limit_recent(limit = 20, offset = nil)
|
||||
recent.limit(limit).offset(offset)
|
||||
end
|
||||
|
||||
def actions
|
||||
ACTIONS.keys
|
||||
end
|
||||
|
||||
def target_types
|
||||
TARGET_TYPES.keys
|
||||
end
|
||||
end
|
||||
|
||||
def visible_to_user?(user = nil)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class PagesDomain < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
|
||||
validates :domain, hostname: true
|
||||
validates :domain, hostname: { allow_numeric_hostname: true }
|
||||
validates :domain, uniqueness: { case_sensitive: false }
|
||||
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
|
||||
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
|
||||
|
@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base
|
|||
|
||||
def validate_pages_domain
|
||||
return unless domain
|
||||
if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
|
||||
if domain.downcase.ends_with?(Settings.pages.host.downcase)
|
||||
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module Users
|
|||
def record_activity
|
||||
Gitlab::UserActivities.record(@author.id)
|
||||
|
||||
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
|
||||
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
- if !project.empty_repo? && can?(current_user, :download_code, project)
|
||||
.project-action-button.dropdown.inline>
|
||||
%button.btn{ 'data-toggle' => 'dropdown' }
|
||||
%button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => 'Download' }
|
||||
= icon('download')
|
||||
= icon("caret-down")
|
||||
%span.sr-only
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- if current_user
|
||||
.project-action-button.dropdown.inline
|
||||
%a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
|
||||
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: 'Create new...', 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => 'Create new...' }
|
||||
= icon('plus')
|
||||
= icon("caret-down")
|
||||
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
= custom_icon('icon_fork')
|
||||
%span Fork
|
||||
- else
|
||||
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
|
||||
= link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do
|
||||
= custom_icon('icon_fork')
|
||||
%span Fork
|
||||
.count-with-arrow
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
.col-md-9
|
||||
.label-light
|
||||
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
|
||||
= link_to "(?)", help_page_path("public_access/public_access")
|
||||
= link_to icon('question-circle'), help_page_path("public_access/public_access")
|
||||
%span.help-block
|
||||
.col-md-3.visibility-select-container
|
||||
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
|
||||
|
@ -92,14 +92,14 @@
|
|||
.form-group
|
||||
= render 'shared/allow_request_access', form: f
|
||||
- if Gitlab.config.lfs.enabled && current_user.admin?
|
||||
.row
|
||||
.row.js-lfs-enabled
|
||||
.col-md-9
|
||||
= f.label :lfs_enabled, 'LFS', class: 'label-light'
|
||||
%span.help-block
|
||||
Git Large File Storage
|
||||
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
|
||||
.col-md-3
|
||||
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
|
||||
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' }
|
||||
|
||||
|
||||
- if Gitlab.config.registry.enabled
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
|
||||
.issue-box
|
||||
- if @bulk_edit
|
||||
.issue-check
|
||||
- if @can_bulk_update
|
||||
.issue-check.hidden
|
||||
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
|
||||
.issue-info-container
|
||||
.issue-title.title
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- @no_container = true
|
||||
- @bulk_edit = can?(current_user, :admin_issue, @project)
|
||||
- @can_bulk_update = can?(current_user, :admin_issue, @project)
|
||||
|
||||
- page_title "Issues"
|
||||
- new_issue_email = @project.new_issue_address(current_user)
|
||||
|
@ -20,6 +20,8 @@
|
|||
.nav-controls
|
||||
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
|
||||
= icon('rss')
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
|
||||
= link_to new_namespace_project_issue_path(@project.namespace,
|
||||
@project,
|
||||
issue: { assignee_id: issues_finder.assignee.try(:id),
|
||||
|
@ -30,6 +32,9 @@
|
|||
New issue
|
||||
= render 'shared/issuable/search_bar', type: :issues
|
||||
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :issues
|
||||
|
||||
.issues-holder
|
||||
= render 'issues'
|
||||
- if new_issue_email
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
|
||||
- if @bulk_edit
|
||||
.issue-check
|
||||
- if @can_bulk_update
|
||||
.issue-check.hidden
|
||||
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
|
||||
|
||||
.issue-info-container
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- @no_container = true
|
||||
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
|
||||
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
|
||||
|
||||
- page_title "Merge Requests"
|
||||
- unless @project.default_issues_tracker?
|
||||
|
@ -18,6 +18,8 @@
|
|||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
.nav-controls
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
|
||||
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
|
||||
- if merge_project
|
||||
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
|
||||
|
@ -25,6 +27,9 @@
|
|||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
|
||||
- if @can_bulk_update
|
||||
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
|
||||
|
||||
.merge-requests-holder
|
||||
= render 'merge_requests'
|
||||
- else
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
.form-group.project-visibility-level-holder
|
||||
= f.label :visibility_level, class: 'label-light' do
|
||||
Visibility Level
|
||||
= link_to icon('question-circle'), help_page_path("public_access/public_access")
|
||||
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }
|
||||
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
|
||||
|
||||
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
- noteable = @sent_notification.noteable
|
||||
- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
|
||||
- noteable_type = @sent_notification.noteable_type.titleize.downcase
|
||||
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
|
||||
|
||||
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
|
||||
|
||||
- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
|
||||
|
||||
%h3.page-title
|
||||
Unsubscribe from #{noteable_type} #{noteable_text}
|
||||
Unsubscribe from #{noteable_type}
|
||||
|
||||
%p
|
||||
= succeed '?' do
|
||||
Are you sure you want to unsubscribe from #{noteable_type}
|
||||
Are you sure you want to unsubscribe from the #{noteable_type}:
|
||||
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
|
||||
|
||||
%p
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
- 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
|
||||
= 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
|
||||
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
|
||||
= button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
|
||||
.block
|
||||
.title
|
||||
Status
|
||||
.filter-item
|
||||
= dropdown_tag("Select status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: { id: "close" } } Closed
|
||||
.block
|
||||
.title
|
||||
Assignee
|
||||
.filter-item
|
||||
- if type == :issues
|
||||
- field_name = "update[assignee_ids][]"
|
||||
- else
|
||||
- field_name = "update[assignee_id]"
|
||||
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
|
||||
.block
|
||||
.title
|
||||
Milestone
|
||||
.filter-item
|
||||
= dropdown_tag("Select milestone", options: { title: "Assign milestone", toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
|
||||
.block
|
||||
.title
|
||||
Labels
|
||||
.filter-item.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: "Apply a label", show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }, label_name: "Select labels", no_default_styles: true
|
||||
.block
|
||||
.title
|
||||
Subscriptions
|
||||
.filter-item
|
||||
= dropdown_tag("Select subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag "update[issuable_ids]", []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
|
|
@ -6,10 +6,6 @@
|
|||
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
- if @bulk_edit
|
||||
.check-all-holder
|
||||
= check_box_tag "check_all_issues", nil, false,
|
||||
class: "check_all_issues left"
|
||||
.issues-other-filters
|
||||
.filter-item.inline
|
||||
- if params[:author_id].present?
|
||||
|
@ -36,35 +32,6 @@
|
|||
.pull-right
|
||||
= render 'shared/sort_dropdown'
|
||||
|
||||
- if @bulk_edit
|
||||
.issues_bulk_update.hide
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Status", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: {id: "close" } } Closed
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'issue-bulk-update-dropdown-toggle js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag 'update[issuable_ids]', []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
.filter-item.inline
|
||||
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
|
||||
- has_labels = @labels && @labels.any?
|
||||
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
|
||||
- if has_labels
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
|
||||
- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
|
||||
- dropdown_data.merge!(data_options)
|
||||
- label_name = local_assigns.fetch(:label_name, "Labels")
|
||||
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
|
||||
- classes << 'js-extra-options' if extra_options
|
||||
- classes << 'js-filter-submit' if filter_submit
|
||||
|
||||
|
@ -20,8 +22,9 @@
|
|||
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
|
||||
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
|
||||
= multi_label_name(selected, "Labels")
|
||||
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
|
||||
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
|
||||
= multi_label_name(selected, label_name)
|
||||
= icon('chevron-down')
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
|
||||
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
- if @bulk_edit
|
||||
.check-all-holder
|
||||
= check_box_tag "check_all_issues", nil, false,
|
||||
class: "check_all_issues left"
|
||||
- if @can_bulk_update
|
||||
.check-all-holder.hidden
|
||||
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
|
||||
.issues-other-filters.filtered-search-wrapper
|
||||
.filtered-search-box
|
||||
- if type != :boards_modal && type != :boards
|
||||
|
@ -110,55 +109,11 @@
|
|||
- elsif type != :boards_modal
|
||||
= render 'shared/sort_dropdown'
|
||||
|
||||
- if @bulk_edit
|
||||
.issues_bulk_update.hide
|
||||
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "reopen" } } Open
|
||||
%li
|
||||
%a{ href: "#", data: { id: "close" } } Closed
|
||||
.filter-item.inline
|
||||
- if type == :issues
|
||||
- field_name = "update[assignee_ids][]"
|
||||
- else
|
||||
- field_name = "update[assignee_id]"
|
||||
|
||||
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
|
||||
%ul
|
||||
%li
|
||||
%a{ href: "#", data: { id: "subscribe" } } Subscribe
|
||||
%li
|
||||
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
|
||||
|
||||
= hidden_field_tag 'update[issuable_ids]', []
|
||||
= hidden_field_tag :state_event, params[:state_event]
|
||||
.filter-item.inline.update-issues-btn
|
||||
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
|
||||
|
||||
- unless type === :boards_modal
|
||||
:javascript
|
||||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssueStatusSelect();
|
||||
new SubscriptionSelect();
|
||||
|
||||
$(document).off('page:restore').on('page:restore', function (event) {
|
||||
if (gl.FilteredSearchManager) {
|
||||
const filteredSearchManager = new gl.FilteredSearchManager();
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
Issuable.init();
|
||||
new gl.IssuableBulkActions({
|
||||
prefixId: 'issue_',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
.js-notification-toggle-btns
|
||||
%div{ class: ("btn-group" if notification_setting.custom?) }
|
||||
- if notification_setting.custom?
|
||||
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
|
||||
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
|
||||
= icon("bell", class: "js-notification-loading")
|
||||
= notification_title(notification_setting.level)
|
||||
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
|
||||
= icon('caret-down')
|
||||
.sr-only Toggle dropdown
|
||||
- else
|
||||
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
|
||||
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
|
||||
= icon("bell", class: "js-notification-loading")
|
||||
= notification_title(notification_setting.level)
|
||||
= icon("caret-down")
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Introduce an Events API
|
||||
merge_request: 11755
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Automatically adjust project settings to match changes in project visibility
|
||||
merge_request: 11831
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add a Rake task to aid in rotating otp_key_base
|
||||
merge_request: 11881
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API'
|
||||
merge_request: 11694
|
||||
author: electroma
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow numeric pages domain
|
||||
merge_request: 11550
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed style on unsubscribe page
|
||||
merge_request:
|
||||
author: Gustav Ernberg
|
|
@ -1,10 +1,7 @@
|
|||
# GitLab Container Registry administration
|
||||
|
||||
> [Introduced][ce-4040] in GitLab 8.8.
|
||||
|
||||
---
|
||||
|
||||
> **Notes:**
|
||||
- [Introduced][ce-4040] in GitLab 8.8.
|
||||
- Container Registry manifest `v1` support was added in GitLab 8.9 to support
|
||||
Docker versions earlier than 1.10.
|
||||
- This document is about the admin guide. To learn how to use GitLab Container
|
||||
|
@ -568,12 +565,25 @@ notifications:
|
|||
backoff: 1000
|
||||
```
|
||||
|
||||
## Changelog
|
||||
## Using self-signed certificates with Container Registry
|
||||
|
||||
**GitLab 8.8 ([source docs][8-8-docs])**
|
||||
If you're using a self-signed certificate with your Container Registry, you
|
||||
might encounter issues during the CI jobs like the following:
|
||||
|
||||
- GitLab Container Registry feature was introduced.
|
||||
```
|
||||
Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority
|
||||
```
|
||||
|
||||
The Docker daemon running the command expects a cert signed by a recognized CA,
|
||||
thus the error above.
|
||||
|
||||
While GitLab doesn't support using self-signed certificates with Container
|
||||
Registry out of the box, it is possible to make it work if you follow
|
||||
[Docker's documentation][docker-insecure]. You may find some additional
|
||||
information in [issue 18239][ce-18239].
|
||||
|
||||
[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
|
||||
[docker-insecure]: https://docs.docker.com/registry/insecure/#using-self-signed-certificates
|
||||
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
|
||||
[restart gitlab]: restart_gitlab.md#installations-from-source
|
||||
[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate
|
||||
|
|
|
@ -15,6 +15,8 @@ following locations:
|
|||
- [Commits](commits.md)
|
||||
- [Deployments](deployments.md)
|
||||
- [Deploy Keys](deploy_keys.md)
|
||||
- [Environments](environments.md)
|
||||
- [Events](events.md)
|
||||
- [Gitignores templates](templates/gitignores.md)
|
||||
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
|
||||
- [Groups](groups.md)
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
# Events
|
||||
|
||||
## Filter parameters
|
||||
|
||||
### Action Types
|
||||
|
||||
Available action types for the `action` parameter are:
|
||||
|
||||
- `created`
|
||||
- `updated`
|
||||
- `closed`
|
||||
- `reopened`
|
||||
- `pushed`
|
||||
- `commented`
|
||||
- `merged`
|
||||
- `joined`
|
||||
- `left`
|
||||
- `destroyed`
|
||||
- `expired`
|
||||
|
||||
Note that these options are downcased.
|
||||
|
||||
### Target Types
|
||||
|
||||
Available target types for the `target_type` parameter are:
|
||||
|
||||
- `issue`
|
||||
- `milestone`
|
||||
- `merge_request`
|
||||
- `note`
|
||||
- `project`
|
||||
- `snippet`
|
||||
- `user`
|
||||
|
||||
Note that these options are downcased.
|
||||
|
||||
### Date formatting
|
||||
|
||||
Dates for the `before` and `after` parameters should be supplied in the following format:
|
||||
|
||||
```
|
||||
YYYY-MM-DD
|
||||
```
|
||||
|
||||
## List currently authenticated user's events
|
||||
|
||||
>**Note:** This endpoint was introduced in GitLab 9.3.
|
||||
|
||||
Get a list of events for the authenticated user.
|
||||
|
||||
```
|
||||
GET /events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":160,
|
||||
"target_type":"Issue",
|
||||
"author_id":25,
|
||||
"data":null,
|
||||
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||
"created_at":"2017-02-09T10:43:19.667Z",
|
||||
"author":{
|
||||
"name":"User 3",
|
||||
"username":"user3",
|
||||
"id":25,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/user3"
|
||||
},
|
||||
"author_username":"user3"
|
||||
},
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":159,
|
||||
"target_type":"Issue",
|
||||
"author_id":21,
|
||||
"data":null,
|
||||
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||
"created_at":"2017-02-09T10:43:19.426Z",
|
||||
"author":{
|
||||
"name":"Test User",
|
||||
"username":"ted",
|
||||
"id":21,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/ted"
|
||||
},
|
||||
"author_username":"ted"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get user contribution events
|
||||
|
||||
>**Note:** Documentation was formerly located in the [Users API pages][users-api].
|
||||
|
||||
Get the contribution events for the specified user, sorted from newest to oldest.
|
||||
|
||||
```
|
||||
GET /users/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID or Username of the user |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## List a Project's visible events
|
||||
|
||||
>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
|
||||
|
||||
Get a list of visible events for a particular project.
|
||||
|
||||
```
|
||||
GET /:project_id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||
|
||||
Example request:
|
||||
|
||||
```
|
||||
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":160,
|
||||
"target_type":"Issue",
|
||||
"author_id":25,
|
||||
"data":null,
|
||||
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||
"created_at":"2017-02-09T10:43:19.667Z",
|
||||
"author":{
|
||||
"name":"User 3",
|
||||
"username":"user3",
|
||||
"id":25,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/user3"
|
||||
},
|
||||
"author_username":"user3"
|
||||
},
|
||||
{
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
"target_id":159,
|
||||
"target_type":"Issue",
|
||||
"author_id":21,
|
||||
"data":null,
|
||||
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||
"created_at":"2017-02-09T10:43:19.426Z",
|
||||
"author":{
|
||||
"name":"Test User",
|
||||
"username":"ted",
|
||||
"id":21,
|
||||
"state":"active",
|
||||
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||
"web_url":"https://gitlab.example.com/ted"
|
||||
},
|
||||
"author_username":"ted"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
[target-types]: #target-types "Target Type parameter"
|
||||
[action-types]: #action-types "Action Type parameter"
|
||||
[date-formatting]: #date-formatting "Date Formatting guidance"
|
||||
[projects-api]: projects.md "Projects API pages"
|
||||
[users-api]: users.md "Users API pages"
|
|
@ -310,143 +310,7 @@ GET /projects/:id/users
|
|||
|
||||
### Get project events
|
||||
|
||||
Get the events for the specified project sorted from newest to oldest. This
|
||||
endpoint can be accessed without authentication if the project is publicly
|
||||
accessible.
|
||||
|
||||
```
|
||||
GET /projects/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
|
||||
|
||||
### Create project
|
||||
|
||||
|
|
|
@ -111,6 +111,7 @@ Parameters:
|
|||
- `author_name` (optional) - Specify the commit author's name
|
||||
- `content` (required) - New file content
|
||||
- `commit_message` (required) - Commit message
|
||||
- `last_commit_id` (optional) - Last known file commit id
|
||||
|
||||
If the commit fails for any reason we return a 400 error with a non-specific
|
||||
error message. Possible causes for a failed commit include:
|
||||
|
|
141
doc/api/users.md
141
doc/api/users.md
|
@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
|
|||
|
||||
### Get user contribution events
|
||||
|
||||
Get the contribution events for the specified user, sorted from newest to oldest.
|
||||
Please refer to the [Events API documentation](events.md#get-user-contribution-events)
|
||||
|
||||
```
|
||||
GET /users/:id/events
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of the user |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 830,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Public project search field",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "opened",
|
||||
"target_id": null,
|
||||
"target_type": null,
|
||||
"author_id": 1,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "john",
|
||||
"data": {
|
||||
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
|
||||
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"ref": "refs/heads/master",
|
||||
"user_id": 1,
|
||||
"user_name": "Dmitriy Zaporozhets",
|
||||
"repository": {
|
||||
"name": "gitlabhq",
|
||||
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
|
||||
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
|
||||
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"message": "Add simple search to projects in public area",
|
||||
"timestamp": "2013-05-13T18:18:08+00:00",
|
||||
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"email": "dmitriy.zaporozhets@gmail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"total_commits_count": 1
|
||||
},
|
||||
"target_title": null
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "closed",
|
||||
"target_id": 840,
|
||||
"target_type": "Issue",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": "Finish & merge Code search PR",
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"project_id": 15,
|
||||
"action_name": "commented on",
|
||||
"target_id": 1312,
|
||||
"target_type": "Note",
|
||||
"author_id": 1,
|
||||
"data": null,
|
||||
"target_title": null,
|
||||
"created_at": "2015-12-04T10:33:58.089Z",
|
||||
"note": {
|
||||
"id": 1312,
|
||||
"body": "What an awesome day!",
|
||||
"attachment": null,
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"created_at": "2015-12-04T10:33:56.698Z",
|
||||
"system": false,
|
||||
"noteable_id": 377,
|
||||
"noteable_type": "Issue"
|
||||
},
|
||||
"author": {
|
||||
"name": "Dmitriy Zaporozhets",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"author_username": "root"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get all impersonation tokens of a user
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Analyze project code quality with Code Climate CLI
|
||||
|
||||
This example shows how to run [Code Climate CLI][cli] on your code by using\
|
||||
This example shows how to run [Code Climate CLI][cli] on your code by using
|
||||
GitLab CI and Docker.
|
||||
|
||||
First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
|
||||
First, you need GitLab Runner with [docker-in-docker executor][dind].
|
||||
|
||||
Once you setup the Runner add new job to `.gitlab-ci.yml`:
|
||||
Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
|
||||
|
||||
```yaml
|
||||
codeclimate:
|
||||
|
@ -25,4 +25,10 @@ codeclimate:
|
|||
This will create a `codeclimate` job in your CI pipeline and will allow you to
|
||||
download and analyze the report artifact in JSON format.
|
||||
|
||||
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
|
||||
extracted and shown right in the merge request widget. [Learn more on code quality
|
||||
diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
|
||||
|
||||
[cli]: https://github.com/codeclimate/codeclimate
|
||||
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
|
||||
[ee]: https://about.gitlab.com/gitlab-ee/
|
||||
|
|
|
@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created
|
|||
tune them.
|
||||
|
||||
Variables can be defined at a global level, but also at a job level. To turn off
|
||||
global defined variables in your job, define an empty array:
|
||||
global defined variables in your job, define an empty hash:
|
||||
|
||||
```yaml
|
||||
job_name:
|
||||
|
@ -345,20 +345,45 @@ All variables are set as environment variables in the build environment, and
|
|||
they are accessible with normal methods that are used to access such variables.
|
||||
In most cases `bash` or `sh` is used to execute the job script.
|
||||
|
||||
To access the variables (predefined and user-defined) in a `bash`/`sh` environment,
|
||||
prefix the variable name with the dollar sign (`$`):
|
||||
To access environment variables, use the syntax for your Runner's [shell][shellexecutors].
|
||||
|
||||
```
|
||||
| Shell | Usage |
|
||||
|----------------------|-----------------|
|
||||
| bash/sh | `$variable` |
|
||||
| windows batch | `%variable%` |
|
||||
| PowerShell | `$env:variable` |
|
||||
|
||||
To access environment variables in bash, prefix the variable name with (`$`):
|
||||
|
||||
```yaml
|
||||
job_name:
|
||||
script:
|
||||
- echo $CI_JOB_ID
|
||||
```
|
||||
|
||||
To access environment variables in **Windows Batch**, surround the variable
|
||||
with (`%`):
|
||||
|
||||
```yaml
|
||||
job_name:
|
||||
script:
|
||||
- echo %CI_JOB_ID%
|
||||
```
|
||||
|
||||
To access environment variables in a **Windows PowerShell** environment, prefix
|
||||
the variable name with (`$env:`):
|
||||
|
||||
```yaml
|
||||
job_name:
|
||||
script:
|
||||
- echo $env:CI_JOB_ID
|
||||
```
|
||||
|
||||
You can also list all environment variables with the `export` command,
|
||||
but be aware that this will also expose the values of all the secret variables
|
||||
you set, in the job log:
|
||||
|
||||
```
|
||||
```yaml
|
||||
job_name:
|
||||
script:
|
||||
- export
|
||||
|
@ -405,3 +430,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
|
|||
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
|
||||
[protected branches]: ../../user/project/protected_branches.md
|
||||
[protected tags]: ../../user/project/protected_tags.md
|
||||
[shellexecutors]: https://docs.gitlab.com/runner/executors/
|
||||
|
|
|
@ -297,6 +297,15 @@ cache:
|
|||
untracked: true
|
||||
```
|
||||
|
||||
If you use **Windows PowerShell** to run your shell scripts you need to replace
|
||||
`$` with `$env:`:
|
||||
|
||||
```yaml
|
||||
cache:
|
||||
key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
## Jobs
|
||||
|
||||
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
|
||||
|
@ -434,7 +443,7 @@ but allows you to define job-specific variables.
|
|||
|
||||
When the `variables` keyword is used on a job level, it overrides the global YAML
|
||||
job variables and predefined ones. To turn off global defined variables
|
||||
in your job, define an empty array:
|
||||
in your job, define an empty hash:
|
||||
|
||||
```yaml
|
||||
job_name:
|
||||
|
@ -909,6 +918,16 @@ job:
|
|||
untracked: true
|
||||
```
|
||||
|
||||
If you use **Windows PowerShell** to run your shell scripts you need to replace
|
||||
`$` with `$env:`:
|
||||
|
||||
```yaml
|
||||
job:
|
||||
artifacts:
|
||||
name: "$env:CI_JOB_STAGE_$env:CI_COMMIT_REF_NAME"
|
||||
untracked: true
|
||||
```
|
||||
|
||||
#### artifacts:when
|
||||
|
||||
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
|
||||
|
|
|
@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th
|
|||
|
||||
You can imagine GitLab as a physical office.
|
||||
|
||||
**The repositories** are the goods GitLab handling.
|
||||
**The repositories** are the goods GitLab handles.
|
||||
They can be stored in a warehouse.
|
||||
This can be either a hard disk, or something more complex, such as a NFS filesystem;
|
||||
|
||||
|
@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
|
|||
|
||||
### Components
|
||||
|
||||
![GitLab Diagram Overview](gitlab_architecture_diagram.png)
|
||||
<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&h=797">
|
||||
|
||||
_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
|
||||
|
||||
|
@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso
|
|||
|
||||
The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
|
||||
|
||||
Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files)
|
||||
Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
|
||||
|
||||
You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/).
|
||||
|
||||
### Installation Folder Summary
|
||||
|
||||
|
|
|
@ -207,7 +207,9 @@ its class in an annotation.
|
|||
>**Note:**
|
||||
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
|
||||
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
|
||||
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md)
|
||||
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
|
||||
>**Note:**
|
||||
If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
|
||||
|
||||
#### Preserving Source IPs
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
|
|||
run the following:
|
||||
|
||||
```bash
|
||||
helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
|
||||
helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
|
||||
```
|
||||
|
||||
- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
|
||||
|
@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE>
|
|||
Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
|
||||
|
||||
```bash
|
||||
helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
|
||||
helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
|
||||
```
|
||||
|
||||
Where:
|
||||
|
|
|
@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
|
|||
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
|
||||
```
|
||||
|
||||
## Rotate Two-factor Authentication (2FA) encryption key
|
||||
|
||||
GitLab stores the secret data enabling 2FA to work in an encrypted database
|
||||
column. The encryption key for this data is known as `otp_key_base`, and is
|
||||
stored in `config/secrets.yml`.
|
||||
|
||||
|
||||
If that file is leaked, but the individual 2FA secrets have not, it's possible
|
||||
to re-encrypt those secrets with a new encryption key. This allows you to change
|
||||
the leaked key without forcing all users to change their 2FA details.
|
||||
|
||||
First, look up the old key. This is in the `config/secrets.yml` file, but
|
||||
**make sure you're working with the production section**. The line you're
|
||||
interested in will look like this:
|
||||
|
||||
```yaml
|
||||
production:
|
||||
otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
```
|
||||
|
||||
Next, generate a new secret:
|
||||
|
||||
```
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-rake secret
|
||||
|
||||
# installation from source
|
||||
bundle exec rake secret RAILS_ENV=production
|
||||
```
|
||||
|
||||
Now you need to stop the GitLab server, back up the existing secrets file and
|
||||
update the database:
|
||||
|
||||
```
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-ctl stop
|
||||
sudo cp config/secrets.yml config/secrets.yml.bak
|
||||
sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
|
||||
|
||||
# installation from source
|
||||
sudo /etc/init.d/gitlab stop
|
||||
cp config/secrets.yml config/secrets.yml.bak
|
||||
bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
|
||||
```
|
||||
|
||||
The `<old key>` value can be read from `config/secrets.yml`; `<new key>` was
|
||||
generated earlier. The **encrypted** values for the user 2FA secrets will be
|
||||
written to the specified `filename` - you can use this to rollback in case of
|
||||
error.
|
||||
|
||||
Finally, change `config/secrets.yml` to set `otp_key_base` to `<new key>` and
|
||||
restart. Again, make sure you're operating in the **production** section.
|
||||
|
||||
```
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-ctl start
|
||||
|
||||
# installation from source
|
||||
sudo /etc/init.d/gitlab start
|
||||
```
|
||||
|
||||
If there are any problems (perhaps using the wrong value for `old_key`), you can
|
||||
restore your backup of `config/secrets.yml` and rollback the changes:
|
||||
|
||||
```
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-ctl stop
|
||||
sudo gitlab-rake gitlab:two_factor:rotate_key:rollback filename=backup.csv
|
||||
sudo cp config/secrets.yml.bak config/secrets.yml
|
||||
sudo gitlab-ctl start
|
||||
|
||||
# installation from source
|
||||
sudo /etc/init.d/gitlab start
|
||||
bundle exec rake gitlab:two_factor:rotate_key:rollback filename=backup.csv RAILS_ENV=production
|
||||
cp config/secrets.yml.bak config/secrets.yml
|
||||
sudo /etc/init.d/gitlab start
|
||||
|
||||
```
|
||||
|
||||
## Clear authentication tokens for all users. Important! Data loss!
|
||||
|
||||
Clear authentication tokens for all users in the GitLab database. This
|
||||
|
|
|
@ -126,7 +126,7 @@ which visibility level you select on project settings.
|
|||
## GitLab CI
|
||||
|
||||
GitLab CI permissions rely on the role the user has in GitLab. There are four
|
||||
permission levels it total:
|
||||
permission levels in total:
|
||||
|
||||
- admin
|
||||
- master
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.7 KiB |
|
@ -1,10 +1,8 @@
|
|||
# Project integrations
|
||||
|
||||
You can find the available integrations under the **Integrations** page by
|
||||
navigating to the cog icon in the upper right corner of your project. You need
|
||||
to have at least [master permission][permissions] on the project.
|
||||
|
||||
![Accessing the integrations](img/accessing_integrations.png)
|
||||
You can find the available integrations under your project's
|
||||
**Settings ➔ Integrations** page. You need to have at least
|
||||
[master permission][permissions] on the project.
|
||||
|
||||
## Project services
|
||||
|
||||
|
|
|
@ -6,15 +6,10 @@ functionality to GitLab.
|
|||
|
||||
## Accessing the project services
|
||||
|
||||
You can find the available services under the **Integrations** page in your
|
||||
project's settings.
|
||||
You can find the available services under your project's
|
||||
**Settings ➔ Integrations** page.
|
||||
|
||||
1. Navigate to the cog icon in the upper right corner of your project. You need
|
||||
to have at least [master permission][permissions] on the project.
|
||||
|
||||
![Accessing the services](img/accessing_integrations.png)
|
||||
|
||||
1. There are more than 20 services to integrate with. Click on the one that you
|
||||
There are more than 20 services to integrate with. Click on the one that you
|
||||
want to configure.
|
||||
|
||||
![Project services list](img/project_services.png)
|
||||
|
|
|
@ -14,11 +14,8 @@ to the webhook URL.
|
|||
Webhooks can be used to update an external issue tracker, trigger CI jobs,
|
||||
update a backup mirror, or even deploy to your production server.
|
||||
|
||||
Navigate to the webhooks page by going to the **Integrations** page from your
|
||||
project's settings which can be found under the wheel icon in the upper right
|
||||
corner.
|
||||
|
||||
![Accessing the integrations](img/accessing_integrations.png)
|
||||
Navigate to the webhooks page by going to your project's
|
||||
**Settings ➔ Integrations**.
|
||||
|
||||
## Webhook endpoint tips
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# GitLab Issues Documentation
|
||||
# Issues documentation
|
||||
|
||||
The GitLab Issue Tracker is an advanced and complete tool
|
||||
for tracking the evolution of a new idea or the process
|
||||
|
@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like:
|
|||
|
||||
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
|
||||
|
||||
## New Issue
|
||||
## New issue
|
||||
|
||||
Read through the [documentation on creating issues](create_new_issue.md).
|
||||
|
||||
## Closing issues
|
||||
|
||||
Read through the distinct ways to [close issues](closing_issues.md) on GitLab.
|
||||
Learn distinct ways to [close issues](closing_issues.md) in GitLab.
|
||||
|
||||
## Create a merge request from an issue
|
||||
|
||||
|
@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr
|
|||
|
||||
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
|
||||
|
||||
### GitLab Issue Board
|
||||
### Issue Board
|
||||
|
||||
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
|
||||
enhance your workflow by organizing and prioritizing issues in GitLab.
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
# Pipelines settings
|
||||
|
||||
To reach the pipelines settings:
|
||||
|
||||
1. Navigate to your project and click the cog icon in the upper right corner.
|
||||
|
||||
![Project settings menu](../img/project_settings_list.png)
|
||||
|
||||
1. Select **Pipelines** from the menu.
|
||||
To reach the pipelines settings navigate to your project's
|
||||
**Settings ➔ CI/CD Pipelines**.
|
||||
|
||||
The following settings can be configured per project.
|
||||
|
||||
|
|
|
@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time.
|
|||
To protect a branch, you need to have at least Master permission level. Note
|
||||
that the `master` branch is protected by default.
|
||||
|
||||
1. Navigate to the main page of the project.
|
||||
1. In the upper right corner, click the settings wheel and select **Protected branches**.
|
||||
|
||||
![Project settings list](img/project_settings_list.png)
|
||||
|
||||
1. Navigate to your project's **Settings ➔ Repository**
|
||||
1. Scroll to find the **Protected branches** section.
|
||||
1. From the **Branch** dropdown menu, select the branch you want to protect and
|
||||
click **Protect**. In the screenshot below, we chose the `develop` branch.
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
|
|||
|
||||
step 'I click link "Fork"' do
|
||||
expect(page).to have_content "Shop"
|
||||
click_link "Fork project"
|
||||
click_link "Fork"
|
||||
end
|
||||
|
||||
step 'I am a member of project "Shop"' do
|
||||
|
|
|
@ -94,6 +94,7 @@ module API
|
|||
mount ::API::DeployKeys
|
||||
mount ::API::Deployments
|
||||
mount ::API::Environments
|
||||
mount ::API::Events
|
||||
mount ::API::Features
|
||||
mount ::API::Files
|
||||
mount ::API::Groups
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
module API
|
||||
class Events < Grape::API
|
||||
include PaginationParams
|
||||
|
||||
helpers do
|
||||
params :event_filter_params do
|
||||
optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
|
||||
optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
|
||||
optional :before, type: Date, desc: 'Include only events created before this date'
|
||||
optional :after, type: Date, desc: 'Include only events created after this date'
|
||||
end
|
||||
|
||||
params :sort_params do
|
||||
optional :sort, type: String, values: %w[asc desc], default: 'desc',
|
||||
desc: 'Return events sorted in ascending and descending order'
|
||||
end
|
||||
|
||||
def present_events(events)
|
||||
events = events.reorder(created_at: params[:sort])
|
||||
|
||||
present paginate(events), with: Entities::Event
|
||||
end
|
||||
end
|
||||
|
||||
resource :events do
|
||||
desc "List currently authenticated user's events" do
|
||||
detail 'This feature was introduced in GitLab 9.3.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get do
|
||||
authenticate!
|
||||
|
||||
events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID or Username of the user'
|
||||
end
|
||||
resource :users do
|
||||
desc 'Get the contribution events of a specified user' do
|
||||
detail 'This feature was introduced in GitLab 8.13.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get ':id/events' do
|
||||
user = find_user(params[:id])
|
||||
not_found!('User') unless user
|
||||
|
||||
events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects, requirements: { id: %r{[^/]+} } do
|
||||
desc "List a Project's visible events" do
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
use :event_filter_params
|
||||
use :sort_params
|
||||
end
|
||||
get ":id/events" do
|
||||
events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
|
||||
|
||||
present_events(events)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,8 @@ module API
|
|||
file_content: attrs[:content],
|
||||
file_content_encoding: attrs[:encoding],
|
||||
author_email: attrs[:author_email],
|
||||
author_name: attrs[:author_name]
|
||||
author_name: attrs[:author_name],
|
||||
last_commit_sha: attrs[:last_commit_id]
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -46,6 +47,7 @@ module API
|
|||
use :simple_file_params
|
||||
requires :content, type: String, desc: 'File content'
|
||||
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
|
||||
optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -111,7 +113,12 @@ module API
|
|||
authorize! :push_code, user_project
|
||||
|
||||
file_params = declared_params(include_missing: false)
|
||||
|
||||
begin
|
||||
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
|
||||
rescue ::Files::UpdateService::FileChangedError => e
|
||||
render_api_error!(e.message, 400)
|
||||
end
|
||||
|
||||
if result[:status] == :success
|
||||
status(200)
|
||||
|
|
|
@ -167,16 +167,6 @@ module API
|
|||
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
|
||||
end
|
||||
|
||||
desc 'Get events for a single project' do
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
get ":id/events" do
|
||||
present paginate(user_project.events.recent), with: Entities::Event
|
||||
end
|
||||
|
||||
desc 'Fork new project for the current user or provided namespace.' do
|
||||
success Entities::Project
|
||||
end
|
||||
|
|
|
@ -328,27 +328,6 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
desc 'Get the contribution events of a specified user' do
|
||||
detail 'This feature was introduced in GitLab 8.13.'
|
||||
success Entities::Event
|
||||
end
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of the user'
|
||||
use :pagination
|
||||
end
|
||||
get ':id/events' do
|
||||
user = User.find_by(id: params[:id])
|
||||
not_found!('User') unless user
|
||||
|
||||
events = user.events.
|
||||
merge(ProjectsFinder.new(current_user: current_user).execute).
|
||||
references(:project).
|
||||
with_associations.
|
||||
recent
|
||||
|
||||
present paginate(events), with: Entities::Event
|
||||
end
|
||||
|
||||
params do
|
||||
requires :user_id, type: Integer, desc: 'The ID of the user'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
module Gitlab
|
||||
# The +otp_key_base+ param is used to encrypt the User#otp_secret attribute.
|
||||
#
|
||||
# When +otp_key_base+ is changed, it invalidates the current encrypted values
|
||||
# of User#otp_secret. This class can be used to decrypt all the values with
|
||||
# the old key, encrypt them with the new key, and and update the database
|
||||
# with the new values.
|
||||
#
|
||||
# For persistence between runs, a CSV file is used with the following columns:
|
||||
#
|
||||
# user_id, old_value, new_value
|
||||
#
|
||||
# Only the encrypted values are stored in this file.
|
||||
#
|
||||
# As users may have their 2FA settings changed at any time, this is only
|
||||
# guaranteed to be safe if run offline.
|
||||
class OtpKeyRotator
|
||||
HEADERS = %w[user_id old_value new_value].freeze
|
||||
|
||||
attr_reader :filename
|
||||
|
||||
# Create a new rotator. +filename+ is used to store values by +calculate!+,
|
||||
# and to update the database with new and old values in +apply!+ and
|
||||
# +rollback!+, respectively.
|
||||
def initialize(filename)
|
||||
@filename = filename
|
||||
end
|
||||
|
||||
def rotate!(old_key:, new_key:)
|
||||
old_key ||= Gitlab::Application.secrets.otp_key_base
|
||||
|
||||
raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key
|
||||
raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64
|
||||
|
||||
write_csv do |csv|
|
||||
ActiveRecord::Base.transaction do
|
||||
User.with_two_factor.in_batches do |relation|
|
||||
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
|
||||
rows.each do |row|
|
||||
user = %i[id ciphertext iv salt].zip(row).to_h
|
||||
new_value = reencrypt(user, old_key, new_key)
|
||||
|
||||
User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
|
||||
csv << [user[:id], user[:ciphertext], new_value]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def rollback!
|
||||
ActiveRecord::Base.transaction do
|
||||
CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
|
||||
User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :old_key, :new_key
|
||||
|
||||
def otp_secret_settings
|
||||
@otp_secret_settings ||= User.encrypted_attributes[:otp_secret]
|
||||
end
|
||||
|
||||
def reencrypt(user, old_key, new_key)
|
||||
original = user[:ciphertext].unpack("m").join
|
||||
opts = {
|
||||
iv: user[:iv].unpack("m").join,
|
||||
salt: user[:salt].unpack("m").join,
|
||||
algorithm: otp_secret_settings[:algorithm],
|
||||
insecure_mode: otp_secret_settings[:insecure_mode]
|
||||
}
|
||||
|
||||
decrypted = Encryptor.decrypt(original, opts.merge(key: old_key))
|
||||
encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key))
|
||||
[encrypted].pack("m")
|
||||
end
|
||||
|
||||
def write_csv(&blk)
|
||||
File.open(filename, "w") do |file|
|
||||
yield CSV.new(file, headers: HEADERS, write_headers: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,5 +19,21 @@ namespace :gitlab do
|
|||
puts "There are currently no users with 2FA enabled.".color(:yellow)
|
||||
end
|
||||
end
|
||||
|
||||
namespace :rotate_key do
|
||||
def rotator
|
||||
@rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename'])
|
||||
end
|
||||
|
||||
desc "Encrypt user OTP secrets with a new encryption key"
|
||||
task apply: :environment do |t, args|
|
||||
rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key'])
|
||||
end
|
||||
|
||||
desc "Rollback to secrets encrypted with the old encryption key"
|
||||
task rollback: :environment do
|
||||
rotator.rollback!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
|
||||
context 'can bulk assign' do
|
||||
before do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
context 'a label' do
|
||||
context 'to all issues' do
|
||||
before do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_labels_dropdown ['bug']
|
||||
update_issues
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'multiple labels' do
|
||||
context 'to all issues' do
|
||||
before do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_labels_dropdown %w(bug feature)
|
||||
update_issues
|
||||
end
|
||||
|
@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
before do
|
||||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
check 'check_all_issues'
|
||||
enable_bulk_update
|
||||
check 'check-all-issues'
|
||||
|
||||
open_labels_dropdown ['bug']
|
||||
update_issues
|
||||
end
|
||||
|
@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
check 'check_all_issues'
|
||||
enable_bulk_update
|
||||
check 'check-all-issues'
|
||||
unmark_labels_in_dropdown %w(bug feature)
|
||||
update_issues
|
||||
end
|
||||
|
@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
enable_bulk_update
|
||||
check_issue issue1
|
||||
unmark_labels_in_dropdown ['bug']
|
||||
update_issues
|
||||
|
@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue2.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
enable_bulk_update
|
||||
check_issue issue1
|
||||
check_issue issue2
|
||||
unmark_labels_in_dropdown ['bug']
|
||||
|
@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
before do
|
||||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps labels' do
|
||||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
open_milestone_dropdown(['First Release'])
|
||||
update_issues
|
||||
|
||||
|
@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'setting a milestone and adding another label' do
|
||||
before do
|
||||
issue1.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps existing label and new label is present' do
|
||||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_milestone_dropdown ['First Release']
|
||||
open_labels_dropdown ['feature']
|
||||
update_issues
|
||||
|
@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << feature
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps existing label and new label is present' do
|
||||
|
@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
open_milestone_dropdown ['First Release']
|
||||
unmark_labels_in_dropdown ['feature']
|
||||
update_issues
|
||||
|
@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << bug
|
||||
issue2.labels << feature
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'keeps labels' do
|
||||
|
@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
|
||||
expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
|
||||
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
open_milestone_dropdown(['No Milestone'])
|
||||
update_issues
|
||||
|
||||
|
@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
context 'toggling checked issues' do
|
||||
before do
|
||||
issue1.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it do
|
||||
|
@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
issue1.labels << feature
|
||||
issue2.labels << bug
|
||||
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
enable_bulk_update
|
||||
end
|
||||
|
||||
it 'applies label from filtered results' do
|
||||
check 'check_all_issues'
|
||||
check 'check-all-issues'
|
||||
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Labels'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select labels'
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
|
||||
|
@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
|
||||
context 'cannot bulk assign labels' do
|
||||
it do
|
||||
expect(page).not_to have_css '.check_all_issues'
|
||||
expect(page).not_to have_button 'Edit Issues'
|
||||
expect(page).not_to have_css '.check-all-issues'
|
||||
expect(page).not_to have_css '.issue-check'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def open_milestone_dropdown(items = [])
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Milestone'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select milestone'
|
||||
wait_for_requests
|
||||
items.map do |item|
|
||||
click_link item
|
||||
|
@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
end
|
||||
|
||||
def open_labels_dropdown(items = [], unmark = false)
|
||||
page.within('.issues_bulk_update') do
|
||||
click_button 'Labels'
|
||||
page.within('.issues-bulk-update') do
|
||||
click_button 'Select labels'
|
||||
wait_for_requests
|
||||
items.map do |item|
|
||||
click_link item
|
||||
|
@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do
|
|||
end
|
||||
|
||||
def update_issues
|
||||
click_button 'Update issues'
|
||||
click_button 'Update all'
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def enable_bulk_update
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
click_button 'Edit Issues'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'sets to closed' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
|
||||
find('.dropdown-menu-status a', text: 'Closed').click
|
||||
|
@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
create_closed
|
||||
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
|
||||
find('.dropdown-menu-status a', text: 'Open').click
|
||||
|
@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'updates to current user' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
click_update_assignee_button
|
||||
|
||||
find('.dropdown-menu-user-link', text: user.username).click
|
||||
|
@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
create_assigned
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
click_update_assignee_button
|
||||
|
||||
click_link 'Unassigned'
|
||||
|
@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
it 'updates milestone' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
|
||||
find('.dropdown-menu-milestone a', text: milestone.title).click
|
||||
click_update_issues_button
|
||||
|
@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
|
||||
expect(first('.issue')).to have_content milestone.title
|
||||
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Issues'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
|
||||
find('.dropdown-menu-milestone a', text: "No Milestone").click
|
||||
click_update_issues_button
|
||||
|
@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
|
|||
end
|
||||
|
||||
def click_update_issues_button
|
||||
find('.update_selected_issues').click
|
||||
find('.update-selected-issues').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
|
|||
end
|
||||
|
||||
def change_status(text)
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.js-issue-status').click
|
||||
find('.dropdown-menu-status a', text: text).click
|
||||
click_update_merge_requests_button
|
||||
end
|
||||
|
||||
def change_assignee(text)
|
||||
find('#check_all_issues').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.js-update-assignee').click
|
||||
wait_for_requests
|
||||
|
||||
|
@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
|
|||
end
|
||||
|
||||
def change_milestone(text)
|
||||
find('#check_all_issues').click
|
||||
find('.issues_bulk_update .js-milestone-select').click
|
||||
click_button 'Edit Merge Requests'
|
||||
find('#check-all-issues').click
|
||||
find('.issues-bulk-update .js-milestone-select').click
|
||||
find('.dropdown-menu-milestone a', text: text).click
|
||||
click_update_merge_requests_button
|
||||
end
|
||||
|
||||
def click_update_merge_requests_button
|
||||
find('.update_selected_issues').click
|
||||
find('.update-selected-issues').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do
|
|||
it 'should show one environment' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
|
||||
expect(page).to have_css('.environments-container')
|
||||
expect(page.all('tbody > tr').length).to eq(1)
|
||||
expect(page.all('.environment-name').length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do
|
|||
it 'should show one environment' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
|
||||
expect(page).to have_css('.environments-container')
|
||||
expect(page.all('tbody > tr').length).to eq(1)
|
||||
expect(page.all('.environment-name').length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
|
|||
visibility_select_container = find('.js-visibility-select')
|
||||
|
||||
expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
|
||||
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
|
||||
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
|
||||
end
|
||||
|
||||
scenario 'project visibility description updates on change' do
|
||||
|
@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do
|
|||
|
||||
expect(visibility_select_container).not_to have_select '.visibility-select'
|
||||
expect(visibility_select_container).to have_content 'Public'
|
||||
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
|
||||
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
|
|||
visit body_link
|
||||
|
||||
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
|
||||
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
|
||||
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
|
||||
expect(page).to have_text(%(Unsubscribe from issue))
|
||||
expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?))
|
||||
expect(issue.subscribed?(recipient, project)).to be_truthy
|
||||
|
||||
click_link 'Unsubscribe'
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe EventsFinder do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
|
||||
let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
|
||||
let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
|
||||
let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
|
||||
let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
|
||||
let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
|
||||
let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
|
||||
let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
|
||||
let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
|
||||
let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
|
||||
|
||||
context 'when targeting a user' do
|
||||
it 'returns events between specified dates filtered on action and type' do
|
||||
events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
|
||||
|
||||
expect(events).to eq([opened_merge_request_event])
|
||||
end
|
||||
|
||||
it 'does not return events the current_user does not have access to' do
|
||||
events = described_class.new(source: user, current_user: other_user).execute
|
||||
|
||||
expect(events).not_to include(opened_merge_request_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when targeting a project' do
|
||||
it 'returns project events between specified dates filtered on action and type' do
|
||||
events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
|
||||
|
||||
expect(events).to eq([closed_issue_event])
|
||||
end
|
||||
|
||||
it 'does not return events the current_user does not have access to' do
|
||||
events = described_class.new(source: project2, current_user: other_user).execute
|
||||
|
||||
expect(events).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
|
@ -257,7 +257,7 @@ describe ProjectsHelper do
|
|||
result = helper.project_feature_access_select(:issues_access_level)
|
||||
expect(result).to include("Disabled")
|
||||
expect(result).to include("Only team members")
|
||||
expect(result).not_to include("Everyone with access")
|
||||
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -272,7 +272,7 @@ describe ProjectsHelper do
|
|||
|
||||
expect(result).to include("Disabled")
|
||||
expect(result).to include("Only team members")
|
||||
expect(result).not_to include("Everyone with access")
|
||||
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
|
||||
expect(result).to have_selector('option[selected]', text: "Only team members")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
|
|||
|
||||
it "describes public projects" do
|
||||
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
|
||||
.to eq "The project can be cloned without any authentication."
|
||||
.to eq "The project can be accessed without any authentication."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
|
||||
it('should render a table with the received pipelines', (done) => {
|
||||
setTimeout(() => {
|
||||
expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
|
||||
expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
|
||||
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
|
||||
expect(this.component.$el.querySelector('.empty-state')).toBe(null);
|
||||
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
|
||||
|
@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
|
|||
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
|
||||
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
|
||||
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
|
||||
expect(this.component.$el.querySelector('table')).toBe(null);
|
||||
expect(this.component.$el.querySelector('.ci-table')).toBe(null);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
|
|
@ -271,7 +271,7 @@ describe('Environment', () => {
|
|||
// wait for next async request
|
||||
setTimeout(() => {
|
||||
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
|
||||
expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
|
||||
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
|
||||
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
|
||||
done();
|
||||
|
|
|
@ -29,6 +29,6 @@ describe('Environment item', () => {
|
|||
},
|
||||
}).$mount();
|
||||
|
||||
expect(component.$el.tagName).toEqual('TABLE');
|
||||
expect(component.$el.getAttribute('class')).toContain('ci-table');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
|
||||
%input{id: 'utf8', name: 'utf8', value: '✓'}
|
||||
%input{id: 'check_all_issues', name: 'check_all_issues'}
|
||||
%input{id: 'check-all-issues', name: 'check-all-issues'}
|
||||
%input{id: 'search', name: 'search'}
|
||||
%input{id: 'author_id', name: 'author_id'}
|
||||
%input{id: 'assignee_id', name: 'assignee_id'}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global Issuable */
|
||||
/* global IssuableIndex */
|
||||
|
||||
import '~/lib/utils/url_utility';
|
||||
import '~/issuable';
|
||||
import '~/issuable_index';
|
||||
|
||||
(() => {
|
||||
const BASE_URL = '/user/project/issues?scope=all&state=closed';
|
||||
|
@ -24,11 +24,11 @@ import '~/issuable';
|
|||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/issuable_filter.html.raw');
|
||||
Issuable.init();
|
||||
IssuableIndex.init();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(window.Issuable).toBeDefined();
|
||||
expect(window.IssuableIndex).toBeDefined();
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
|
@ -43,7 +43,7 @@ import '~/issuable';
|
|||
it('should contain only the default parameters', () => {
|
||||
spyOn(gl.utils, 'visitUrl');
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ import '~/issuable';
|
|||
spyOn(gl.utils, 'visitUrl');
|
||||
|
||||
updateForm({ search: 'broken' }, $filtersForm);
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
const params = `${DEFAULT_PARAMS}&search=broken`;
|
||||
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
|
@ -64,14 +64,14 @@ import '~/issuable';
|
|||
// initial filter
|
||||
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
|
||||
// update filter
|
||||
updateForm({ label_name: 'Frontend' }, $filtersForm);
|
||||
|
||||
Issuable.filterResults($filtersForm);
|
||||
IssuableIndex.filterResults($filtersForm);
|
||||
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
|
||||
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::OtpKeyRotator do
|
||||
let(:file) { Tempfile.new("otp-key-rotator-test") }
|
||||
let(:filename) { file.path }
|
||||
let(:old_key) { Gitlab::Application.secrets.otp_key_base }
|
||||
let(:new_key) { "00" * 32 }
|
||||
let!(:users) { create_list(:user, 5, :two_factor) }
|
||||
|
||||
after do
|
||||
file.close
|
||||
file.unlink
|
||||
end
|
||||
|
||||
def data
|
||||
CSV.read(filename)
|
||||
end
|
||||
|
||||
def build_row(user, applied = false)
|
||||
[user.id.to_s, encrypt_otp(user, old_key), encrypt_otp(user, new_key)]
|
||||
end
|
||||
|
||||
def encrypt_otp(user, key)
|
||||
opts = {
|
||||
value: user.otp_secret,
|
||||
iv: user.encrypted_otp_secret_iv.unpack("m").join,
|
||||
salt: user.encrypted_otp_secret_salt.unpack("m").join,
|
||||
algorithm: 'aes-256-cbc',
|
||||
insecure_mode: true,
|
||||
key: key
|
||||
}
|
||||
[Encryptor.encrypt(opts)].pack("m")
|
||||
end
|
||||
|
||||
subject(:rotator) { described_class.new(filename) }
|
||||
|
||||
describe '#rotate!' do
|
||||
subject(:rotation) { rotator.rotate!(old_key: old_key, new_key: new_key) }
|
||||
|
||||
it 'stores the calculated values in a spreadsheet' do
|
||||
rotation
|
||||
|
||||
expect(data).to match_array(users.map {|u| build_row(u) })
|
||||
end
|
||||
|
||||
context 'new key is too short' do
|
||||
let(:new_key) { "00" * 31 }
|
||||
|
||||
it { expect { rotation }.to raise_error(ArgumentError) }
|
||||
end
|
||||
|
||||
context 'new key is the same as the old key' do
|
||||
let(:new_key) { old_key }
|
||||
|
||||
it { expect { rotation }.to raise_error(ArgumentError) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rollback!' do
|
||||
it 'updates rows to the old value' do
|
||||
file.puts("#{users[0].id},old,new")
|
||||
file.close
|
||||
|
||||
rotator.rollback!
|
||||
|
||||
expect(users[0].reload.encrypted_otp_secret).to eq('old')
|
||||
expect(users[1].reload.encrypted_otp_secret).not_to eq('old')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ describe PagesDomain, models: true do
|
|||
end
|
||||
|
||||
describe 'validate domain' do
|
||||
subject { build(:pages_domain, domain: domain) }
|
||||
subject(:pages_domain) { build(:pages_domain, domain: domain) }
|
||||
|
||||
context 'is unique' do
|
||||
let(:domain) { 'my.domain.com' }
|
||||
|
@ -14,36 +14,25 @@ describe PagesDomain, models: true do
|
|||
it { is_expected.to validate_uniqueness_of(:domain) }
|
||||
end
|
||||
|
||||
context 'valid domain' do
|
||||
let(:domain) { 'my.domain.com' }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
{
|
||||
'my.domain.com' => true,
|
||||
'123.456.789' => true,
|
||||
'0x12345.com' => true,
|
||||
'0123123' => true,
|
||||
'_foo.com' => false,
|
||||
'reserved.com' => false,
|
||||
'a.reserved.com' => false,
|
||||
nil => false
|
||||
}.each do |value, validity|
|
||||
context "domain #{value.inspect} validity" do
|
||||
before do
|
||||
allow(Settings.pages).to receive(:host).and_return('reserved.com')
|
||||
end
|
||||
|
||||
context 'valid hexadecimal-looking domain' do
|
||||
let(:domain) { '0x12345.com'}
|
||||
let(:domain) { value }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
it { expect(pages_domain.valid?).to eq(validity) }
|
||||
end
|
||||
|
||||
context 'no domain' do
|
||||
let(:domain) { nil }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
context 'invalid domain' do
|
||||
let(:domain) { '0123123' }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
|
||||
context 'domain from .example.com' do
|
||||
let(:domain) { 'my.domain.com' }
|
||||
|
||||
before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue