Merge branch 'master' into 'dispatcher-cleanup'
# Conflicts: # config/webpack.config.js
This commit is contained in:
commit
9cb3ababe7
|
@ -1,3 +1,4 @@
|
|||
<!---
|
||||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
|
@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker:
|
|||
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
|
||||
Please remove this notice if you're confident your issue isn't a duplicate.
|
||||
|
||||
------
|
||||
--->
|
||||
|
||||
### Summary
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { __ } from '../../locale';
|
|||
import Sidebar from '../../right_sidebar';
|
||||
import eventHub from '../../sidebar/event_hub';
|
||||
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
|
||||
import assignees from '../../sidebar/components/assignees/assignees';
|
||||
import assignees from '../../sidebar/components/assignees/assignees.vue';
|
||||
import DueDateSelectors from '../../due_date_select';
|
||||
import './sidebar/remove_issue';
|
||||
import IssuableContext from '../../issuable_context';
|
||||
|
|
|
@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store';
|
|||
|
||||
Vue.use(Translate);
|
||||
|
||||
$(() => {
|
||||
export default () => {
|
||||
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
|
||||
|
||||
gl.cycleAnalyticsApp = new Vue({
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
components: {
|
||||
|
@ -132,4 +132,4 @@ $(() => {
|
|||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import repoCommitSection from './repo_commit_section.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import repoTree from './ide_repo_tree.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import branchesTree from './ide_project_branches_tree.vue';
|
||||
import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import repoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import repoFile from './repo_file.vue';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
import { treeList } from '../stores/utils';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import projectTree from './ide_project_tree.vue';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import flash, { hideFlash } from '../../flash';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import flash, { hideFlash } from '~/flash';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import modal from '../../vue_shared/components/modal.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import commitFilesList from './commit_sidebar/list.vue';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import modal from '../../vue_shared/components/modal.vue';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
/* global monaco */
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import flash from '../../flash';
|
||||
import flash from '~/flash';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import LineHighlighter from '../../line_highlighter';
|
||||
import syntaxHighlight from '../../syntax_highlight';
|
||||
import LineHighlighter from '~/line_highlighter';
|
||||
import syntaxHighlight from '~/syntax_highlight';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import { visitUrl } from '../../lib/utils/url_utility';
|
||||
import flash from '../../flash';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import flash from '~/flash';
|
||||
import service from '../services';
|
||||
import * as types from './mutation_types';
|
||||
import { stripHtml } from '../../lib/utils/text_utility';
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
|
||||
$els.each(function(i, dropdown) {
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
|
||||
$dropdown = $(dropdown);
|
||||
$dropdownContainer = $dropdown.closest('.labels-filter');
|
||||
$toggleText = $dropdown.find('.dropdown-toggle-text');
|
||||
|
@ -53,13 +53,6 @@ export default class LabelsSelect {
|
|||
.map(function () {
|
||||
return this.value;
|
||||
}).get();
|
||||
if (issueUpdateURL != null) {
|
||||
issueURLSplit = issueUpdateURL.split('/');
|
||||
}
|
||||
if (issueUpdateURL) {
|
||||
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
|
||||
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
|
||||
}
|
||||
const handleClick = options.handleClick;
|
||||
|
||||
$sidebarLabelTooltip.tooltip();
|
||||
|
@ -91,14 +84,17 @@ export default class LabelsSelect {
|
|||
$loading.fadeOut();
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$selectbox.hide();
|
||||
data.issueURLSplit = issueURLSplit;
|
||||
data.issueUpdateURL = issueUpdateURL;
|
||||
labelCount = 0;
|
||||
if (data.labels.length) {
|
||||
template = labelHTMLTemplate(data);
|
||||
if (data.labels.length && issueUpdateURL) {
|
||||
template = LabelsSelect.getLabelTemplate({
|
||||
labels: data.labels,
|
||||
issueUpdateURL,
|
||||
});
|
||||
labelCount = data.labels.length;
|
||||
}
|
||||
else {
|
||||
template = labelNoneHTMLTemplate;
|
||||
template = '<span class="no-value">None</span>';
|
||||
}
|
||||
$value.removeAttr('style').html(template);
|
||||
$sidebarCollapsedValue.text(labelCount);
|
||||
|
@ -418,6 +414,26 @@ export default class LabelsSelect {
|
|||
this.bindEvents();
|
||||
}
|
||||
|
||||
static getLabelTemplate(tplData) {
|
||||
// We could use ES6 template string here
|
||||
// and properly indent markup for readability
|
||||
// but that also introduces unintended white-space
|
||||
// so best approach is to use traditional way of
|
||||
// concatenation
|
||||
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
|
||||
const tpl = _.template([
|
||||
'<% _.each(labels, function(label){ %>',
|
||||
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
|
||||
'<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
|
||||
'<%- label.title %>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'<% }); %>',
|
||||
].join(''));
|
||||
|
||||
return tpl(tplData);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import UsersSelect from '~/users_select';
|
||||
import { isMetaClick } from '~/lib/utils/common_utils';
|
||||
import { __ } from '../../../../locale';
|
||||
import flash from '../../../../flash';
|
||||
import axios from '../../../../lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import flash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export default class Todos {
|
||||
constructor() {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initCycleAnalytics);
|
|
@ -1,224 +0,0 @@
|
|||
export default {
|
||||
name: 'Assignees',
|
||||
data() {
|
||||
return {
|
||||
defaultRenderCount: 5,
|
||||
defaultMaxCounter: 99,
|
||||
showLess: true,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
users: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
firstUser() {
|
||||
return this.users[0];
|
||||
},
|
||||
hasMoreThanTwoAssignees() {
|
||||
return this.users.length > 2;
|
||||
},
|
||||
hasMoreThanOneAssignee() {
|
||||
return this.users.length > 1;
|
||||
},
|
||||
hasAssignees() {
|
||||
return this.users.length > 0;
|
||||
},
|
||||
hasNoUsers() {
|
||||
return !this.users.length;
|
||||
},
|
||||
hasOneUser() {
|
||||
return this.users.length === 1;
|
||||
},
|
||||
renderShowMoreSection() {
|
||||
return this.users.length > this.defaultRenderCount;
|
||||
},
|
||||
numberOfHiddenAssignees() {
|
||||
return this.users.length - this.defaultRenderCount;
|
||||
},
|
||||
isHiddenAssignees() {
|
||||
return this.numberOfHiddenAssignees > 0;
|
||||
},
|
||||
hiddenAssigneesLabel() {
|
||||
return `+ ${this.numberOfHiddenAssignees} more`;
|
||||
},
|
||||
collapsedTooltipTitle() {
|
||||
const maxRender = Math.min(this.defaultRenderCount, this.users.length);
|
||||
const renderUsers = this.users.slice(0, maxRender);
|
||||
const names = renderUsers.map(u => u.name);
|
||||
|
||||
if (this.users.length > maxRender) {
|
||||
names.push(`+ ${this.users.length - maxRender} more`);
|
||||
}
|
||||
|
||||
return names.join(', ');
|
||||
},
|
||||
sidebarAvatarCounter() {
|
||||
let counter = `+${this.users.length - 1}`;
|
||||
|
||||
if (this.users.length > this.defaultMaxCounter) {
|
||||
counter = `${this.defaultMaxCounter}+`;
|
||||
}
|
||||
|
||||
return counter;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
assignSelf() {
|
||||
this.$emit('assign-self');
|
||||
},
|
||||
toggleShowLess() {
|
||||
this.showLess = !this.showLess;
|
||||
},
|
||||
renderAssignee(index) {
|
||||
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
|
||||
},
|
||||
avatarUrl(user) {
|
||||
return user.avatar || user.avatar_url || gon.default_avatar_url;
|
||||
},
|
||||
assigneeUrl(user) {
|
||||
return `${this.rootPath}${user.username}`;
|
||||
},
|
||||
assigneeAlt(user) {
|
||||
return `${user.name}'s avatar`;
|
||||
},
|
||||
assigneeUsername(user) {
|
||||
return `@${user.username}`;
|
||||
},
|
||||
shouldRenderCollapsedAssignee(index) {
|
||||
const firstTwo = this.users.length <= 2 && index <= 2;
|
||||
|
||||
return index === 0 || firstTwo;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-user"
|
||||
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
:title="collapsedTooltipTitle"
|
||||
>
|
||||
<i
|
||||
v-if="hasNoUsers"
|
||||
aria-label="No Assignee"
|
||||
class="fa fa-user"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
v-for="(user, index) in users"
|
||||
v-if="shouldRenderCollapsedAssignee(index)"
|
||||
>
|
||||
<img
|
||||
width="24"
|
||||
class="avatar avatar-inline s24"
|
||||
:alt="assigneeAlt(user)"
|
||||
:src="avatarUrl(user)"
|
||||
/>
|
||||
<span class="author">
|
||||
{{ user.name }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="hasMoreThanTwoAssignees"
|
||||
class="btn-link"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="avatar-counter sidebar-avatar-counter"
|
||||
>
|
||||
{{ sidebarAvatarCounter }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="value hide-collapsed">
|
||||
<template v-if="hasNoUsers">
|
||||
<span class="assign-yourself no-value">
|
||||
No assignee
|
||||
<template v-if="editable">
|
||||
-
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
@click="assignSelf"
|
||||
>
|
||||
assign yourself
|
||||
</button>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="hasOneUser">
|
||||
<a
|
||||
class="author_link bold"
|
||||
:href="assigneeUrl(firstUser)"
|
||||
>
|
||||
<img
|
||||
width="32"
|
||||
class="avatar avatar-inline s32"
|
||||
:alt="assigneeAlt(firstUser)"
|
||||
:src="avatarUrl(firstUser)"
|
||||
/>
|
||||
<span class="author">
|
||||
{{ firstUser.name }}
|
||||
</span>
|
||||
<span class="username">
|
||||
{{ assigneeUsername(firstUser) }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="user-list">
|
||||
<div
|
||||
class="user-item"
|
||||
v-for="(user, index) in users"
|
||||
v-if="renderAssignee(index)"
|
||||
>
|
||||
<a
|
||||
class="user-link has-tooltip"
|
||||
data-placement="bottom"
|
||||
:href="assigneeUrl(user)"
|
||||
:data-title="user.name"
|
||||
>
|
||||
<img
|
||||
width="32"
|
||||
class="avatar avatar-inline s32"
|
||||
:alt="assigneeAlt(user)"
|
||||
:src="avatarUrl(user)"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="renderShowMoreSection"
|
||||
class="user-list-more"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
@click="toggleShowLess"
|
||||
>
|
||||
<template v-if="showLess">
|
||||
{{ hiddenAssigneesLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
- show less
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,232 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'Assignees',
|
||||
props: {
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
users: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultRenderCount: 5,
|
||||
defaultMaxCounter: 99,
|
||||
showLess: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstUser() {
|
||||
return this.users[0];
|
||||
},
|
||||
hasMoreThanTwoAssignees() {
|
||||
return this.users.length > 2;
|
||||
},
|
||||
hasMoreThanOneAssignee() {
|
||||
return this.users.length > 1;
|
||||
},
|
||||
hasAssignees() {
|
||||
return this.users.length > 0;
|
||||
},
|
||||
hasNoUsers() {
|
||||
return !this.users.length;
|
||||
},
|
||||
hasOneUser() {
|
||||
return this.users.length === 1;
|
||||
},
|
||||
renderShowMoreSection() {
|
||||
return this.users.length > this.defaultRenderCount;
|
||||
},
|
||||
numberOfHiddenAssignees() {
|
||||
return this.users.length - this.defaultRenderCount;
|
||||
},
|
||||
isHiddenAssignees() {
|
||||
return this.numberOfHiddenAssignees > 0;
|
||||
},
|
||||
hiddenAssigneesLabel() {
|
||||
return `+ ${this.numberOfHiddenAssignees} more`;
|
||||
},
|
||||
collapsedTooltipTitle() {
|
||||
const maxRender = Math.min(this.defaultRenderCount, this.users.length);
|
||||
const renderUsers = this.users.slice(0, maxRender);
|
||||
const names = renderUsers.map(u => u.name);
|
||||
|
||||
if (this.users.length > maxRender) {
|
||||
names.push(`+ ${this.users.length - maxRender} more`);
|
||||
}
|
||||
|
||||
return names.join(', ');
|
||||
},
|
||||
sidebarAvatarCounter() {
|
||||
let counter = `+${this.users.length - 1}`;
|
||||
|
||||
if (this.users.length > this.defaultMaxCounter) {
|
||||
counter = `${this.defaultMaxCounter}+`;
|
||||
}
|
||||
|
||||
return counter;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
assignSelf() {
|
||||
this.$emit('assign-self');
|
||||
},
|
||||
toggleShowLess() {
|
||||
this.showLess = !this.showLess;
|
||||
},
|
||||
renderAssignee(index) {
|
||||
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
|
||||
},
|
||||
avatarUrl(user) {
|
||||
return user.avatar || user.avatar_url || gon.default_avatar_url;
|
||||
},
|
||||
assigneeUrl(user) {
|
||||
return `${this.rootPath}${user.username}`;
|
||||
},
|
||||
assigneeAlt(user) {
|
||||
return `${user.name}'s avatar`;
|
||||
},
|
||||
assigneeUsername(user) {
|
||||
return `@${user.username}`;
|
||||
},
|
||||
shouldRenderCollapsedAssignee(index) {
|
||||
const firstTwo = this.users.length <= 2 && index <= 2;
|
||||
|
||||
return index === 0 || firstTwo;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-user"
|
||||
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
:title="collapsedTooltipTitle"
|
||||
>
|
||||
<i
|
||||
v-if="hasNoUsers"
|
||||
aria-label="No Assignee"
|
||||
class="fa fa-user"
|
||||
>
|
||||
</i>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
v-for="(user, index) in users"
|
||||
v-if="shouldRenderCollapsedAssignee(index)"
|
||||
:key="user.id"
|
||||
>
|
||||
<img
|
||||
width="24"
|
||||
class="avatar avatar-inline s24"
|
||||
:alt="assigneeAlt(user)"
|
||||
:src="avatarUrl(user)"
|
||||
/>
|
||||
<span class="author">
|
||||
{{ user.name }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="hasMoreThanTwoAssignees"
|
||||
class="btn-link"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="avatar-counter sidebar-avatar-counter"
|
||||
>
|
||||
{{ sidebarAvatarCounter }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="value hide-collapsed">
|
||||
<template v-if="hasNoUsers">
|
||||
<span class="assign-yourself no-value">
|
||||
No assignee
|
||||
<template v-if="editable">
|
||||
-
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
@click="assignSelf"
|
||||
>
|
||||
assign yourself
|
||||
</button>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="hasOneUser">
|
||||
<a
|
||||
class="author_link bold"
|
||||
:href="assigneeUrl(firstUser)"
|
||||
>
|
||||
<img
|
||||
width="32"
|
||||
class="avatar avatar-inline s32"
|
||||
:alt="assigneeAlt(firstUser)"
|
||||
:src="avatarUrl(firstUser)"
|
||||
/>
|
||||
<span class="author">
|
||||
{{ firstUser.name }}
|
||||
</span>
|
||||
<span class="username">
|
||||
{{ assigneeUsername(firstUser) }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="user-list">
|
||||
<div
|
||||
class="user-item"
|
||||
v-for="(user, index) in users"
|
||||
v-if="renderAssignee(index)"
|
||||
:key="user.id"
|
||||
>
|
||||
<a
|
||||
class="user-link has-tooltip"
|
||||
data-container="body"
|
||||
data-placement="bottom"
|
||||
:href="assigneeUrl(user)"
|
||||
:data-title="user.name"
|
||||
>
|
||||
<img
|
||||
width="32"
|
||||
class="avatar avatar-inline s32"
|
||||
:alt="assigneeAlt(user)"
|
||||
:src="avatarUrl(user)"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="renderShowMoreSection"
|
||||
class="user-list-more"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
@click="toggleShowLess"
|
||||
>
|
||||
<template v-if="showLess">
|
||||
{{ hiddenAssigneesLabel }}
|
||||
</template>
|
||||
<template v-else>
|
||||
- show less
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Flash from '../../../flash';
|
||||
import AssigneeTitle from './assignee_title';
|
||||
import Assignees from './assignees';
|
||||
import Assignees from './assignees.vue';
|
||||
import Store from '../../stores/sidebar_store';
|
||||
import eventHub from '../../event_hub';
|
||||
|
||||
|
@ -28,8 +28,8 @@ export default {
|
|||
},
|
||||
},
|
||||
components: {
|
||||
'assignee-title': AssigneeTitle,
|
||||
assignees: Assignees,
|
||||
AssigneeTitle,
|
||||
Assignees,
|
||||
},
|
||||
methods: {
|
||||
assignSelf() {
|
||||
|
|
|
@ -103,6 +103,7 @@
|
|||
.issuable-show-labels {
|
||||
a {
|
||||
margin-bottom: 5px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
|
||||
.color-label {
|
||||
|
@ -116,6 +117,12 @@
|
|||
}
|
||||
|
||||
&.has-labels {
|
||||
// this font size is a fix to
|
||||
// prevent unintended spacing between labels
|
||||
// which shows up when rendering markup has white-space
|
||||
// characters present.
|
||||
// see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
|
||||
font-size: 0;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
|
|||
|
||||
before_action :require_pages_enabled!
|
||||
before_action :authorize_update_pages!, except: [:show]
|
||||
before_action :domain, only: [:show, :destroy, :verify]
|
||||
before_action :domain, except: [:new, :create]
|
||||
|
||||
def show
|
||||
end
|
||||
|
@ -24,8 +24,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController
|
|||
redirect_to project_pages_domain_path(@project, @domain)
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@domain = @project.pages_domains.create(pages_domain_params)
|
||||
@domain = @project.pages_domains.create(create_params)
|
||||
|
||||
if @domain.valid?
|
||||
redirect_to project_pages_domain_path(@project, @domain)
|
||||
|
@ -34,6 +37,16 @@ class Projects::PagesDomainsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @domain.update(update_params)
|
||||
redirect_to project_pages_path(@project),
|
||||
status: 302,
|
||||
notice: 'Domain was updated'
|
||||
else
|
||||
render 'edit'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@domain.destroy
|
||||
|
||||
|
@ -49,12 +62,12 @@ class Projects::PagesDomainsController < Projects::ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def pages_domain_params
|
||||
params.require(:pages_domain).permit(
|
||||
:certificate,
|
||||
:key,
|
||||
:domain
|
||||
)
|
||||
def create_params
|
||||
params.require(:pages_domain).permit(:key, :certificate, :domain)
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.require(:pages_domain).permit(:key, :certificate)
|
||||
end
|
||||
|
||||
def domain
|
||||
|
|
|
@ -49,7 +49,7 @@ module Ci
|
|||
ref_protected: 1
|
||||
}
|
||||
|
||||
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at
|
||||
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
|
||||
|
||||
# Searches for runners matching the given query.
|
||||
#
|
||||
|
@ -157,7 +157,7 @@ module Ci
|
|||
end
|
||||
|
||||
def update_cached_info(values)
|
||||
values = values&.slice(:version, :revision, :platform, :architecture) || {}
|
||||
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {}
|
||||
values[:contacted_at] = Time.now
|
||||
|
||||
cache_attributes(values)
|
||||
|
|
|
@ -15,6 +15,8 @@ module Projects
|
|||
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
|
||||
end
|
||||
|
||||
ensure_wiki_exists if enabling_wiki?
|
||||
|
||||
if project.update_attributes(params.except(:default_branch))
|
||||
if project.previous_changes.include?('path')
|
||||
project.rename_repo
|
||||
|
@ -52,5 +54,18 @@ module Projects
|
|||
project.repository.exists? &&
|
||||
new_branch && new_branch != project.default_branch
|
||||
end
|
||||
|
||||
def enabling_wiki?
|
||||
return false if @project.wiki_enabled?
|
||||
|
||||
params[:project_feature_attributes][:wiki_access_level].to_i > ProjectFeature::DISABLED
|
||||
end
|
||||
|
||||
def ensure_wiki_exists
|
||||
ProjectWiki.new(project, project.owner).wiki
|
||||
rescue ProjectWiki::CouldNotCreateWikiError
|
||||
log_error("Could not create wiki for #{project.full_name}")
|
||||
Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
= runner.description
|
||||
%td
|
||||
= runner.version
|
||||
%td
|
||||
= runner.ip_address
|
||||
%td
|
||||
- if runner.shared?
|
||||
n/a
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
%th Runner token
|
||||
%th Description
|
||||
%th Version
|
||||
%th IP Address
|
||||
%th Projects
|
||||
%th Jobs
|
||||
%th Tags
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
- page_title "Cycle Analytics"
|
||||
- content_for :page_specific_javascripts do
|
||||
= webpack_bundle_tag('common_vue')
|
||||
= webpack_bundle_tag('cycle_analytics')
|
||||
|
||||
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
|
||||
- if @cycle_analytics_no_data
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
|
||||
- if @domain.errors.any?
|
||||
#error_explanation
|
||||
.alert.alert-danger
|
||||
- @domain.errors.full_messages.each do |msg|
|
||||
%p= msg
|
||||
- if @domain.errors.any?
|
||||
#error_explanation
|
||||
.alert.alert-danger
|
||||
- @domain.errors.full_messages.each do |msg|
|
||||
%p= msg
|
||||
|
||||
.form-group
|
||||
= f.label :domain, class: 'control-label' do
|
||||
Domain
|
||||
.col-sm-10
|
||||
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
|
||||
|
||||
- if Gitlab.config.pages.external_https
|
||||
.form-group
|
||||
= f.label :certificate, class: 'control-label' do
|
||||
Certificate (PEM)
|
||||
.col-sm-10
|
||||
= f.text_area :certificate, rows: 5, class: 'form-control'
|
||||
%span.help-inline Upload a certificate for your domain with all intermediates
|
||||
|
||||
.form-group
|
||||
= f.label :domain, class: 'control-label' do
|
||||
Domain
|
||||
= f.label :key, class: 'control-label' do
|
||||
Key (PEM)
|
||||
.col-sm-10
|
||||
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
|
||||
|
||||
- if Gitlab.config.pages.external_https
|
||||
.form-group
|
||||
= f.label :certificate, class: 'control-label' do
|
||||
Certificate (PEM)
|
||||
.col-sm-10
|
||||
= f.text_area :certificate, rows: 5, class: 'form-control'
|
||||
%span.help-inline Upload a certificate for your domain with all intermediates
|
||||
|
||||
.form-group
|
||||
= f.label :key, class: 'control-label' do
|
||||
Key (PEM)
|
||||
.col-sm-10
|
||||
= f.text_area :key, rows: 5, class: 'form-control'
|
||||
%span.help-inline Upload a private key for your certificate
|
||||
- else
|
||||
.nothing-here-block
|
||||
Support for custom certificates is disabled.
|
||||
Ask your system's administrator to enable it.
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Create New Domain', class: "btn btn-save"
|
||||
= f.text_area :key, rows: 5, class: 'form-control'
|
||||
%span.help-inline Upload a private key for your certificate
|
||||
- else
|
||||
.nothing-here-block
|
||||
Support for custom certificates is disabled.
|
||||
Ask your system's administrator to enable it.
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
- add_to_breadcrumbs "Pages", project_pages_path(@project)
|
||||
- breadcrumb_title @domain.domain
|
||||
- page_title @domain.domain
|
||||
%h3.page_title
|
||||
= @domain.domain
|
||||
%hr.clearfix
|
||||
%div
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
|
||||
= render 'form', { f: f }
|
||||
.form-actions
|
||||
= f.submit 'Save Changes', class: "btn btn-save"
|
|
@ -1,6 +1,10 @@
|
|||
- add_to_breadcrumbs "Pages", project_pages_path(@project)
|
||||
- page_title 'New Pages Domain'
|
||||
%h3.page_title
|
||||
New Pages Domain
|
||||
%hr.clearfix
|
||||
%div
|
||||
= render 'form'
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
|
||||
= render 'form', { f: f }
|
||||
.form-actions
|
||||
= f.submit 'Create New Domain', class: "btn btn-save"
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
- add_to_breadcrumbs "Pages", project_pages_path(@project)
|
||||
- breadcrumb_title @domain.domain
|
||||
- page_title "#{@domain.domain}", 'Pages Domains'
|
||||
|
||||
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
|
||||
- if verification_enabled && @domain.unverified?
|
||||
%p.alert.alert-warning
|
||||
|
@ -8,6 +11,7 @@
|
|||
|
||||
%h3.page-title
|
||||
Pages Domain
|
||||
= link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
|
||||
|
||||
.table-holder
|
||||
%table.table
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
Token
|
||||
.col-sm-10
|
||||
= f.text_field :token, class: 'form-control', readonly: true
|
||||
.form-group
|
||||
= label_tag :ip_address, class: 'control-label' do
|
||||
IP Address
|
||||
.col-sm-10
|
||||
= f.text_field :ip_address, class: 'form-control', readonly: true
|
||||
.form-group
|
||||
= label_tag :description, class: 'control-label' do
|
||||
Description
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
%tr
|
||||
%td Version
|
||||
%td= @runner.version
|
||||
%tr
|
||||
%td IP Address
|
||||
%td= @runner.ip_address
|
||||
%tr
|
||||
%td Revision
|
||||
%td= @runner.revision
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display Runner IP Address
|
||||
merge_request: 17286
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix code and wiki search results pages when non-ASCII text is displayed
|
||||
merge_request: 17413
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable filtering MR list based on clicked label in MR sidebar
|
||||
merge_request: 17390
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make sure wiki exists when it's enabled
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Assignees vue component missing data container
|
||||
merge_request: 17426
|
||||
author: George Tsiolis
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Pages custom domain: allow update of key/certificate'
|
||||
merge_request: 17376
|
||||
author: rfwatson
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Assignees vue component
|
||||
merge_request: 16952
|
||||
author: George Tsiolis
|
||||
type: performance
|
|
@ -103,4 +103,6 @@ Doorkeeper.configure do
|
|||
# Some applications require dynamic query parameters on their request_uri
|
||||
# set to true if you want this to be allowed
|
||||
# wildcard_redirect_uri false
|
||||
|
||||
base_controller 'ApplicationController'
|
||||
end
|
||||
|
|
|
@ -12,9 +12,14 @@ unless Sidekiq.server?
|
|||
config.lograge.logger = ActiveSupport::Logger.new(filename)
|
||||
# Add request parameters to log output
|
||||
config.lograge.custom_options = lambda do |event|
|
||||
params = event.payload[:params]
|
||||
.except(*%w(controller action format))
|
||||
.each_pair
|
||||
.map { |k, v| { key: k, value: v } }
|
||||
|
||||
payload = {
|
||||
time: event.time.utc.iso8601(3),
|
||||
params: event.payload[:params].except(*%w(controller action format)),
|
||||
params: params,
|
||||
remote_ip: event.payload[:remote_ip],
|
||||
user_id: event.payload[:user_id],
|
||||
username: event.payload[:username]
|
||||
|
|
|
@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
resource :pages, only: [:show, :destroy] do
|
||||
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
|
||||
resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
|
||||
member do
|
||||
post :verify
|
||||
end
|
||||
|
|
|
@ -44,7 +44,6 @@ function generateEntries() {
|
|||
|
||||
const manualEntries = {
|
||||
balsamiq_viewer: './blob/balsamiq_viewer.js',
|
||||
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
|
||||
environments: './environments/environments_bundle.js',
|
||||
monitoring: './monitoring/monitoring_bundle.js',
|
||||
mr_notes: './mr_notes/index.js',
|
||||
|
@ -59,7 +58,6 @@ function generateEntries() {
|
|||
terminal: './terminal/terminal_bundle.js',
|
||||
two_factor_auth: './two_factor_auth.js',
|
||||
|
||||
|
||||
common: './commons/index.js',
|
||||
common_vue: './vue_shared/vue_resource_interceptor.js',
|
||||
locale: './locale/index.js',
|
||||
|
@ -225,6 +223,33 @@ const config = {
|
|||
return `${moduleNames[0]}-${hash.substr(0, 6)}`;
|
||||
}),
|
||||
|
||||
// create cacheable common library bundle for all vue chunks
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'common_vue',
|
||||
chunks: [
|
||||
'boards',
|
||||
'deploy_keys',
|
||||
'environments',
|
||||
'filtered_search',
|
||||
'groups',
|
||||
'monitoring',
|
||||
'mr_notes',
|
||||
'notebook_viewer',
|
||||
'pdf_viewer',
|
||||
'pipelines',
|
||||
'pipelines_details',
|
||||
'registry_list',
|
||||
'ide',
|
||||
'schedule_form',
|
||||
'schedules_index',
|
||||
'sidebar',
|
||||
'vue_merge_request_widget',
|
||||
],
|
||||
minChunks: function(module, count) {
|
||||
return module.resource && (/vue_shared/).test(module.resource);
|
||||
},
|
||||
}),
|
||||
|
||||
// create cacheable common library bundles
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
names: ['main', 'common', 'webpack_runtime'],
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddIpAddressToRunner < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :ci_runners, :ip_address, :string
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180216121030) do
|
||||
ActiveRecord::Schema.define(version: 20180222043024) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -437,6 +437,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do
|
|||
t.boolean "run_untagged", default: true, null: false
|
||||
t.boolean "locked", default: false, null: false
|
||||
t.integer "access_level", default: 0, null: false
|
||||
t.string "ip_address"
|
||||
end
|
||||
|
||||
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
toc: false
|
||||
comments: false
|
||||
---
|
||||
|
||||
|
@ -8,15 +7,9 @@ comments: false
|
|||
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
|
||||
platform for software development!
|
||||
|
||||
GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans.
|
||||
|
||||
With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
|
||||
|
||||
Every feature available in Libre is also available in Starter, Premium, and Ultimate.
|
||||
Starter features are also available in Premium and Ultimate, and Premium features are also
|
||||
available in Ultimate.
|
||||
|
||||
GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold.
|
||||
GitLab offers the most scalable Git-based fully integrated platform for
|
||||
software development, with flexible products and subscriptions.
|
||||
To understand what features you have access to, check the [GitLab subscriptions](#gitlab-subscriptions) below.
|
||||
|
||||
## Shortcuts to GitLab's most visited docs
|
||||
|
||||
|
@ -124,8 +117,6 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
|
|||
- [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
|
||||
- [Trello Power-Up](integration/trello_power_up.md): Integrate with GitLab's Trello Power-Up
|
||||
|
||||
----
|
||||
|
||||
## Administrator documentation
|
||||
|
||||
[Administration documentation](administration/index.md) applies to admin users of GitLab
|
||||
|
@ -143,3 +134,42 @@ Learn how to contribute to GitLab:
|
|||
- [Development](development/README.md): All styleguides and explanations how to contribute.
|
||||
- [Legal](legal/README.md): Contributor license agreements.
|
||||
- [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs.
|
||||
|
||||
## GitLab subscriptions
|
||||
|
||||
You have two options to use GitLab:
|
||||
|
||||
- GitLab self-hosted: Install, administer, and maintain your own GitLab instance.
|
||||
- GitLab.com: GitLab's SaaS offering. You don't need to install anything to use GitLab.com,
|
||||
you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab
|
||||
straight away.
|
||||
|
||||
### GitLab self-hosted
|
||||
|
||||
With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
|
||||
|
||||
Every feature available in Libre is also available in Starter, Premium, and Ultimate.
|
||||
Starter features are also available in Premium and Ultimate, and Premium features are also
|
||||
available in Ultimate.
|
||||
|
||||
### GitLab.com
|
||||
|
||||
GitLab.com is hosted, managed, and administered by GitLab, Inc., with
|
||||
[free and paid subscriptions](https://about.gitlab.com/gitlab-com/) for individuals
|
||||
and teams: Free, Bronze, Silver, and Gold.
|
||||
|
||||
GitLab.com subscriptions grants access
|
||||
to the same features available in GitLab self-hosted, **expect
|
||||
[administration](administration/index.md) tools and settings**:
|
||||
|
||||
- GitLab.com Free includes the same features available in GitLab Libre
|
||||
- GitLab.com Bronze includes the same features available in GitLab Starter
|
||||
- GitLab.com Silver includes the same features available in GitLab Premium
|
||||
- GitLab.com Gold includes the same features available in GitLab Ultimate
|
||||
|
||||
For supporting the open source community and encouraging the development of
|
||||
open source projects, GitLab grants access to **Gold** features
|
||||
for all GitLab.com **public** projects, regardless of the subscription.
|
||||
|
||||
To know more about GitLab subscriptions and licensing, please refer to the
|
||||
[GitLab Product Marketing Handbook](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers).
|
||||
|
|
|
@ -56,6 +56,9 @@ future GitLab releases.**
|
|||
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
|
||||
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
|
||||
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
|
||||
| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
|
||||
| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
|
||||
| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
|
||||
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
|
||||
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
|
||||
| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source |
|
||||
|
|
|
@ -507,6 +507,7 @@ This is the entry point for our store. You can use the following as a guide:
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import * as mutations from './mutations';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -514,6 +515,7 @@ Vue.use(Vuex);
|
|||
export default new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: {
|
||||
users: [],
|
||||
},
|
||||
|
|
|
@ -109,8 +109,7 @@ in your SAML IdP:
|
|||
1. Change the value of `issuer` to a unique name, which will identify the application
|
||||
to the IdP.
|
||||
|
||||
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
|
||||
installed GitLab via Omnibus or from source respectively.
|
||||
1. For the changes to take effect, you must [reconfigure][] GitLab if you installed via Omnibus or [restart GitLab][] if you installed from source.
|
||||
|
||||
1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
|
||||
in `issuer`.
|
||||
|
|
|
@ -36,12 +36,16 @@ GFM honors the markdown specification in how [paragraphs and line breaks are han
|
|||
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
|
||||
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
|
||||
|
||||
Roses are red [followed by two or more spaces]
|
||||
[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
|
||||
[//]: # (They are needed for the Markdown text to render correctly.)
|
||||
Roses are red [followed by two or more spaces]
|
||||
Violets are blue
|
||||
|
||||
Sugar is sweet
|
||||
|
||||
Roses are red
|
||||
[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
|
||||
[//]: # (They are needed for the Markdown text to render correctly.)
|
||||
Roses are red
|
||||
Violets are blue
|
||||
|
||||
Sugar is sweet
|
||||
|
|
|
@ -34,7 +34,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
|
|||
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
|
||||
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
|
||||
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
|
||||
- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
|
||||
- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
|
||||
|
||||
## Use cases
|
||||
|
||||
|
|
|
@ -9,16 +9,22 @@ module API
|
|||
Gitlab::CurrentSettings.runners_registration_token)
|
||||
end
|
||||
|
||||
def get_runner_version_from_params
|
||||
return unless params['info'].present?
|
||||
|
||||
attributes_for_keys(%w(name version revision platform architecture), params['info'])
|
||||
end
|
||||
|
||||
def authenticate_runner!
|
||||
forbidden! unless current_runner
|
||||
|
||||
current_runner.update_cached_info(get_runner_version_from_params)
|
||||
current_runner
|
||||
.update_cached_info(get_runner_details_from_request)
|
||||
end
|
||||
|
||||
def get_runner_details_from_request
|
||||
return get_runner_ip unless params['info'].present?
|
||||
|
||||
attributes_for_keys(%w(name version revision platform architecture), params['info'])
|
||||
.merge(get_runner_ip)
|
||||
end
|
||||
|
||||
def get_runner_ip
|
||||
{ ip_address: request.ip }
|
||||
end
|
||||
|
||||
def current_runner
|
||||
|
|
|
@ -16,7 +16,8 @@ module API
|
|||
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
|
||||
end
|
||||
post '/' do
|
||||
attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
|
||||
attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
|
||||
.merge(get_runner_details_from_request)
|
||||
|
||||
runner =
|
||||
if runner_registration_token_valid?
|
||||
|
@ -30,7 +31,6 @@ module API
|
|||
return forbidden! unless runner
|
||||
|
||||
if runner.id
|
||||
runner.update(get_runner_version_from_params)
|
||||
present runner, with: Entities::RunnerRegistrationDetails
|
||||
else
|
||||
not_found!
|
||||
|
|
|
@ -59,7 +59,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def pages(limit: nil)
|
||||
@repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
|
||||
@repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
|
||||
if is_enabled
|
||||
gitaly_get_all_pages
|
||||
else
|
||||
|
@ -68,9 +68,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039
|
||||
def page(title:, version: nil, dir: nil)
|
||||
@repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
|
||||
@repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
|
||||
if is_enabled
|
||||
gitaly_find_page(title: title, version: version, dir: dir)
|
||||
else
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Gitlab
|
||||
class SearchResults
|
||||
class FoundBlob
|
||||
include EncodingHelper
|
||||
|
||||
attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
|
||||
|
||||
def initialize(opts = {})
|
||||
|
@ -9,7 +11,7 @@ module Gitlab
|
|||
@basename = opts.fetch(:basename, nil)
|
||||
@ref = opts.fetch(:ref, nil)
|
||||
@startline = opts.fetch(:startline, nil)
|
||||
@data = opts.fetch(:data, nil)
|
||||
@data = encode_utf8(opts.fetch(:data, nil))
|
||||
@per_page = opts.fetch(:per_page, 20)
|
||||
@project_id = opts.fetch(:project_id, nil)
|
||||
end
|
||||
|
|
|
@ -21,6 +21,8 @@ else
|
|||
File.open(hook_path, 'w') do |file|
|
||||
IO.copy_stream(DATA, file)
|
||||
end
|
||||
|
||||
File.chmod(0755, hook_path)
|
||||
end
|
||||
|
||||
# Toggle the harness on or off
|
||||
|
|
|
@ -34,6 +34,8 @@ describe Oauth::AuthorizationsController do
|
|||
end
|
||||
|
||||
context 'with valid params' do
|
||||
render_views
|
||||
|
||||
it 'returns 200 code and renders view' do
|
||||
get :new, params
|
||||
|
||||
|
|
|
@ -53,6 +53,66 @@ describe Projects::PagesDomainsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET edit' do
|
||||
it "displays the 'edit' page" do
|
||||
get(:edit, request_params.merge(id: pages_domain.domain))
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to render_template('edit')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH update' do
|
||||
before do
|
||||
controller.instance_variable_set(:@domain, pages_domain)
|
||||
end
|
||||
|
||||
let(:pages_domain_params) do
|
||||
attributes_for(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate)
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
request_params.merge(id: pages_domain.domain, pages_domain: pages_domain_params)
|
||||
end
|
||||
|
||||
it 'updates the domain' do
|
||||
expect(pages_domain)
|
||||
.to receive(:update)
|
||||
.with(pages_domain_params)
|
||||
.and_return(true)
|
||||
|
||||
patch(:update, params)
|
||||
end
|
||||
|
||||
it 'redirects to the project page' do
|
||||
patch(:update, params)
|
||||
|
||||
expect(flash[:notice]).to eq 'Domain was updated'
|
||||
expect(response).to redirect_to(project_pages_path(project))
|
||||
end
|
||||
|
||||
context 'the domain is invalid' do
|
||||
it 'renders the edit action' do
|
||||
allow(pages_domain).to receive(:update).and_return(false)
|
||||
|
||||
patch(:update, params)
|
||||
|
||||
expect(response).to render_template('edit')
|
||||
end
|
||||
end
|
||||
|
||||
context 'the parameters include the domain' do
|
||||
it 'renders 400 Bad Request' do
|
||||
expect(pages_domain)
|
||||
.to receive(:update)
|
||||
.with(hash_not_including(:domain))
|
||||
.and_return(true)
|
||||
|
||||
patch(:update, params.deep_merge(pages_domain: { domain: 'abc' }))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST verify' do
|
||||
let(:params) { request_params.merge(id: pages_domain.domain) }
|
||||
|
||||
|
|
|
@ -160,6 +160,37 @@ feature 'Pages' do
|
|||
|
||||
expect(page).to have_content('my.test.domain.com')
|
||||
end
|
||||
|
||||
describe 'updating the certificate for an existing domain' do
|
||||
let!(:domain) do
|
||||
create(:pages_domain, :with_key, :with_certificate, project: project)
|
||||
end
|
||||
|
||||
it 'allows the certificate to be updated' do
|
||||
visit project_pages_path(project)
|
||||
|
||||
within('#content-body') { click_link 'Details' }
|
||||
click_link 'Edit'
|
||||
click_button 'Save Changes'
|
||||
|
||||
expect(page).to have_content('Domain was updated')
|
||||
end
|
||||
|
||||
context 'when the certificate is invalid' do
|
||||
it 'tells the user what the problem is' do
|
||||
visit project_pages_path(project)
|
||||
|
||||
within('#content-body') { click_link 'Details' }
|
||||
click_link 'Edit'
|
||||
fill_in 'Certificate (PEM)', with: 'invalid data'
|
||||
click_button 'Save Changes'
|
||||
|
||||
expect(page).to have_content('Certificate must be a valid PEM certificate')
|
||||
expect(page).to have_content('Certificate misses intermediates')
|
||||
expect(page).to have_content("Key doesn't match the certificate")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -367,23 +367,6 @@ describe 'Pipelines', :js do
|
|||
expect(build.reload).to be_canceled
|
||||
end
|
||||
end
|
||||
|
||||
context 'dropdown jobs list' do
|
||||
it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
|
||||
find('.js-builds-dropdown-button').click
|
||||
dropdown_item = find('.mini-pipeline-graph-dropdown-item').native
|
||||
|
||||
%i(alt control).each do |meta_key|
|
||||
page.driver.browser.action
|
||||
.key_down(meta_key)
|
||||
.click(dropdown_item)
|
||||
.key_up(meta_key)
|
||||
.perform
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.js-ci-action-icon')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import LabelsSelect from '~/labels_select';
|
||||
|
||||
const mockUrl = '/foo/bar/url';
|
||||
|
||||
const mockLabels = [
|
||||
{
|
||||
id: 26,
|
||||
title: 'Foo Label',
|
||||
description: 'Foobar',
|
||||
color: '#BADA55',
|
||||
text_color: '#FFFFFF',
|
||||
},
|
||||
];
|
||||
|
||||
describe('LabelsSelect', () => {
|
||||
describe('getLabelTemplate', () => {
|
||||
const label = mockLabels[0];
|
||||
let $labelEl;
|
||||
|
||||
beforeEach(() => {
|
||||
$labelEl = $(LabelsSelect.getLabelTemplate({
|
||||
labels: mockLabels,
|
||||
issueUpdateURL: mockUrl,
|
||||
}));
|
||||
});
|
||||
|
||||
it('generated label item template has correct label URL', () => {
|
||||
expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
|
||||
});
|
||||
|
||||
it('generated label item template has correct label title', () => {
|
||||
expect($labelEl.find('span.label').text()).toBe(label.title);
|
||||
});
|
||||
|
||||
it('generated label item template has label description as title attribute', () => {
|
||||
expect($labelEl.find('span.label').attr('title')).toBe(label.description);
|
||||
});
|
||||
|
||||
it('generated label item template has correct label styles', () => {
|
||||
expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import Assignee from '~/sidebar/components/assignees/assignees';
|
||||
import Assignee from '~/sidebar/components/assignees/assignees.vue';
|
||||
import UsersMock from './mock_data';
|
||||
import UsersMockHelper from '../helpers/user_mock_data_helper';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# coding: utf-8
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ProjectSearchResults do
|
||||
|
@ -105,6 +106,32 @@ describe Gitlab::ProjectSearchResults do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the search returns non-ASCII data' do
|
||||
context 'with UTF-8' do
|
||||
let(:results) { project.repository.search_files_by_content("файл", 'master') }
|
||||
|
||||
it 'returns results as UTF-8' do
|
||||
expect(subject.filename).to eq('encoding/russian.rb')
|
||||
expect(subject.basename).to eq('encoding/russian')
|
||||
expect(subject.ref).to eq('master')
|
||||
expect(subject.startline).to eq(1)
|
||||
expect(subject.data).to eq("Хороший файл")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ISO-8859-1' do
|
||||
let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
|
||||
|
||||
it 'returns results as UTF-8' do
|
||||
expect(subject.filename).to eq('encoding/iso8859.txt')
|
||||
expect(subject.basename).to eq('encoding/iso8859')
|
||||
expect(subject.ref).to eq('master')
|
||||
expect(subject.startline).to eq(1)
|
||||
expect(subject.data).to eq("Äü\n\nfoo")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when filename has extension" do
|
||||
let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
|
||||
|
||||
|
|
|
@ -122,6 +122,15 @@ describe API::Runner do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "sets the runner's ip_address" do
|
||||
post api('/runners'),
|
||||
{ token: registration_token },
|
||||
{ 'REMOTE_ADDR' => '123.111.123.111' }
|
||||
|
||||
expect(response).to have_gitlab_http_status 201
|
||||
expect(Ci::Runner.first.ip_address).to eq('123.111.123.111')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v4/runners' do
|
||||
|
@ -422,6 +431,15 @@ describe API::Runner do
|
|||
end
|
||||
end
|
||||
|
||||
it "sets the runner's ip_address" do
|
||||
post api('/jobs/request'),
|
||||
{ token: runner.token },
|
||||
{ 'User-Agent' => user_agent, 'REMOTE_ADDR' => '123.222.123.222' }
|
||||
|
||||
expect(response).to have_gitlab_http_status 201
|
||||
expect(runner.reload.ip_address).to eq('123.222.123.222')
|
||||
end
|
||||
|
||||
context 'when concurrently updating a job' do
|
||||
before do
|
||||
expect_any_instance_of(Ci::Build).to receive(:run!)
|
||||
|
|
|
@ -123,6 +123,40 @@ describe Projects::UpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when we update project but not enabling a wiki' do
|
||||
it 'does not try to create an empty wiki' do
|
||||
FileUtils.rm_rf(project.wiki.repository.path)
|
||||
|
||||
result = update_project(project, user, { name: 'test1' })
|
||||
|
||||
expect(result).to eq({ status: :success })
|
||||
expect(project.wiki_repository_exists?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enabling a wiki' do
|
||||
it 'creates a wiki' do
|
||||
project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
|
||||
FileUtils.rm_rf(project.wiki.repository.path)
|
||||
|
||||
result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
|
||||
|
||||
expect(result).to eq({ status: :success })
|
||||
expect(project.wiki_repository_exists?).to be true
|
||||
expect(project.wiki_enabled?).to be true
|
||||
end
|
||||
|
||||
it 'logs an error and creates a metric when wiki can not be created' do
|
||||
project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
|
||||
|
||||
expect_any_instance_of(ProjectWiki).to receive(:wiki).and_raise(ProjectWiki::CouldNotCreateWikiError)
|
||||
expect_any_instance_of(described_class).to receive(:log_error).with("Could not create wiki for #{project.full_name}")
|
||||
expect(Gitlab::Metrics).to receive(:counter)
|
||||
|
||||
update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating a project that contains container images' do
|
||||
before do
|
||||
stub_container_registry_config(enabled: true)
|
||||
|
|
Loading…
Reference in New Issue