Commit cdb46f59 authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into ee-46381-dropdown-mr-widget

* master: (108 commits)
  Resolve conflicts in app/views/admin/dashboard/index.html.haml
  Resolve conflicts in app/controllers/admin/dashboard_controller.rb
  Resolve conflict in LICENSE
  fixed copy to cliboard button in embedded snippets
  Migrates EE-only js components to single file components
  Use less aggressive sticking for DB load balancing
  Fix Error 500 viewing admin page due to statement timeouts
  Fix Error 500 viewing admin page due to statement timeouts
  Grant privileges after database is created
  Only setup db in the first checkout!
  Add a CI job to test ce-upgrade-ee migration path
  resolve conflicts in locale/gitlab.pot
  resolve conflicts in app/views/layouts/nav/sidebar/_project.html.haml
  Project Sidebar: Split CI/CD into CI/CD and Operations
  Fix GPM content types for Doorkeeper
  Remove docker pull prefix from registry clipboard feature
  Port of gitlab-ce!18941
  Add contact sales option for Epics
  Remove unnecessary section-100 css class from application_row.vue
  Resolve other conflicts in locale/gitlab.pot
  ...
parents 6f5bf2cf ace6b918
......@@ -278,7 +278,6 @@ stages:
<<: *use-pg
variables:
SETUP_DB: "false"
CREATE_DB_USER: "true"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
......@@ -322,7 +321,7 @@ stages:
.migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee
- git checkout -f FETCH_HEAD
......@@ -331,13 +330,31 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- git checkout $CI_COMMIT_SHA
- git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
- date
- bundle exec rake db:migrate
.migration-paths-upgrade-ce-to-ee: &migration-paths-upgrade-ce-to-ee
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
SETUP_DB: "false"
script:
- CE_HEAD=$(ruby -r./scripts/ee_specific_check/ee_specific_check -e'puts EESpecificCheck.fetch_remote_ce_branch')
- git checkout -f $CE_HEAD
- . scripts/utils.sh
- . scripts/prepare_build.sh
- date
- setup_db
- date
- git checkout -f $CI_COMMIT_SHA
- date
- . scripts/prepare_build.sh
- date
- bundle exec rake db:migrate
##
# Trigger a package build in omnibus-gitlab repository
#
......@@ -738,6 +755,14 @@ migration:path-mysql:
<<: *migration-paths
<<: *use-mysql
migration:upgrade-pg-ce-to-ee:
<<: *migration-paths-upgrade-ce-to-ee
<<: *use-pg
migration:upgrade-mysql-ce-to-ee:
<<: *migration-paths-upgrade-ce-to-ee
<<: *use-mysql
.db-rollback: &db-rollback
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
......
......@@ -42,7 +42,9 @@ When removing columns, tables, indexes or other structures:
- [ ] Has been reviewed by a Database specialist
- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
/label ~database
......@@ -31,8 +31,8 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
- [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker)
......@@ -212,9 +212,10 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Estimate time to fix | Guidance |
......@@ -224,16 +225,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
#### Specific Priority guidance
| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
Severity labels help us clearly communicate the impact of a ~bug on users.
......@@ -244,14 +236,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Specific Severity guidance
#### Severity impact guidance
| Label | Security Impact |
|-------|-------------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
| Label | Security Impact | Availability / Performance Impact |
|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% users impacted (possible company extinction level event) | |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
### Label for community contributors (~"Accepting Merge Requests")
......
......@@ -42,8 +42,3 @@ THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the
applicable component.
---
All Documentation content that resides under the doc/ directory of this
repository is licensed under Creative Commons: CC BY-SA 4.0.
......@@ -103,6 +103,8 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
......
......@@ -179,7 +179,7 @@
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0 table-section section-100"
class="alert alert-danger alert-block append-bottom-0"
role="gridcell"
>
<div>
......
......@@ -32,17 +32,17 @@ export default {
<template>
<div :class="className">
{{ actionText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
<template v-if="editedBy">
by
{{ s__('ByAuthor|by') }}
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
</div>
</template>
......@@ -62,6 +62,21 @@ export default {
<template>
<div class="note-header-info">
<div
v-if="includeToggle"
class="discussion-actions">
<button
@click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button">
<i
:class="toggleChevronClass"
class="fa"
aria-hidden="true">
</i>
{{ __('Toggle discussion') }}
</button>
</div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
......@@ -95,20 +110,5 @@ export default {
</i>
</span>
</span>
<div
v-if="includeToggle"
class="discussion-actions">
<button
@click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button">
<i
:class="toggleChevronClass"
class="fa"
aria-hidden="true">
</i>
Toggle discussion
</button>
</div>
</div>
</template>
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
......@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = {
top: 20,
right: 20,
right: 10,
bottom: 30,
left: 50
left: 40
};
ContributorsGraph.prototype.x_domain = null;
......@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = [];
ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
const marginWidth = this.MARGIN.left + this.MARGIN.right;
return baseWidth - parentPaddingWidth - marginWidth;
};
ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data;
};
......@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master');
const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1;
this.update_content = this.update_content.bind(this);
this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
this.height = 200;
this.x = null;
this.y = null;
......@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}
ContributorsMasterGraph.prototype.process_dates = function(data) {
var dates;
dates = this.get_dates(data);
const dates = this.get_dates(data);
this.parse_dates(data);
return ContributorsGraph.set_dates(dates);
};
......@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate;
parseDate = d3.timeParse("%Y-%m-%d");
const parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
......@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_svg = function() {
return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
this.svg = d3.select("#contributors-master")
.append("svg")
.attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
.attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr("class", "tint-box")
.append("g")
.attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
return this.svg;
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
......@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) {
const $parentElements = $('.person');
this.data = data1;
// Don't split graph size in half for mobile devices.
if ($(window).width() < 768) {
this.width = $('.content').width() - 80;
if ($(window).width() < 790) {
this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else {
this.width = ($('.content').width() / 2) - 100;
this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
}
this.height = 200;
this.x = null;
......@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) {
var parseDate;
parseDate = d3.timeParse("%Y-%m-%d");
const parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
......@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_svg = function() {
var persons = document.querySelectorAll('.person');
const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
this.svg = d3.select(this.list_item)
.append("svg")
.attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
.attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
.attr("class", "spark")
.append("g")
.attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
return this.svg;
};
ContributorsAuthorGraph.prototype.draw_path = function(data) {
......
<script>
import _ from 'underscore';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlModal,
},
props: {
deleteWikiUrl: {
type: String,
required: true,
default: '',
},
pageTitle: {
type: String,
required: true,
default: '',
},
csrfToken: {
type: String,
required: true,
default: '',
},
},
computed: {
message() {
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
},
title() {
return sprintf(
s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'),
{
pageTitle: _.escape(this.pageTitle),
},
false,
);
},
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<gl-modal
id="delete-wiki-modal"
:header-title-text="title"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
@submit="onSubmit"
>
{{ message }}
<form
ref="form"
:action="deleteWikiUrl"
method="post"
class="form-horizontal js-requires-input"
>
<input
ref="method"
type="hidden"
name="_method"
value="delete"
/>
<input
type="hidden"
name="authenticity_token"
:value="csrfToken"
/>
</form>
</gl-modal>
</template>
import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
import deleteWikiModal from './components/delete_wiki_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button');
if (deleteWikiButton) {
Vue.use(Translate);
const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
const deleteModal = new Vue({ // eslint-disable-line
el: deleteWikiModalEl,
data: {
deleteWikiUrl: '',
},
render(createElement) {
return createElement(deleteWikiModal, {
props: {
pageTitle,
deleteWikiUrl,
csrfToken: csrf.token,
},
});
},
});
}
});
......@@ -28,11 +28,6 @@
isOpen: false,
};
},
computed: {
clipboardText() {
return `docker pull ${this.repo.location}`;
},
},
methods: {
...mapActions([
'fetchRepos',
......@@ -84,7 +79,7 @@
<clipboard-button
v-if="repo.location"
:text="clipboardText"
:text="repo.location"
:title="repo.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
......
......@@ -56,10 +56,6 @@
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
clipboardText(text) {
return `docker pull ${text}`;
},
showError(message) {
Flash(errorMessages[message]);
},
......@@ -89,7 +85,7 @@
<clipboard-button
v-if="item.location"
:title="item.location"
:text="clipboardText(item.location)"
:text="item.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</td>
......
......@@ -26,7 +26,7 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.vue';
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
......
......@@ -210,3 +210,15 @@
margin-left: -$size;
}
}
/*
* Mixin that fixes wrapping issues with long strings (e.g. URLs)
*
* Note: the width needs to be set for it to work in Firefox
*/
@mixin overflow-break-word {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
max-width: 100%;
}
......@@ -67,7 +67,8 @@
padding: 8px 40px;
}
.embed-toggle {
.embed-toggle,
.snippet-clipboard-btn {
height: 35px;
}
}
......@@ -329,6 +329,9 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
// as a fallback, hide overflow content so that dragging and dropping still works
overflow: hidden;
&:not(:last-child) {
margin-bottom: 5px;
}
......@@ -355,14 +358,13 @@
}
.card-title {
@include overflow-break-word();
margin: 0 30px 0 0;
font-size: 1em;
line-height: inherit;
a {
color: $gl-text-color;
word-wrap: break-word;
word-break: break-word;
margin-right: 2px;
}
}
......@@ -531,8 +533,8 @@
}
.issuable-header-text {
@include overflow-break-word();
padding-right: 35px;
word-break: break-word;
> strong {
font-weight: $gl-font-weight-bold;
......
......@@ -407,10 +407,6 @@ ul.notes {
.note-header {
display: flex;
justify-content: space-between;
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
.note-header-info {
......@@ -473,11 +469,6 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
......
class Admin::DashboardController < Admin::ApplicationController
prepend ::EE::Admin::DashboardController
include CountHelper
def index
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
......
......@@ -183,4 +183,8 @@ class Projects::PipelinesController < Projects::ApplicationController
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
end
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
end
......@@ -24,6 +24,15 @@ module AutoDevopsHelper
end
end
def cluster_ingress_ip(project)
project
.cluster_ingresses
.where("external_ip is not null")
.limit(1)
.pluck(:external_ip)
.first
end
private
def missing_auto_devops_domain?(project)
......
module CountHelper
def approximate_count_with_delimiters(model)
number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
end
end
module IssuablesHelper
prepend EE::IssuablesHelper
include GitlabRoutingHelper
def sidebar_gutter_toggle_icon
......@@ -390,9 +392,7 @@ module IssuablesHelper
editable: can_edit_issuable,
currentUser: UserSerializer.new.represent(current_user),
rootPath: root_path,
fullPath: @project.full_path,
weightOptions: Issue.weight_options,
weightNoneValue: Issue::WEIGHT_NONE
fullPath: @project.full_path
}
end
......
......@@ -259,6 +259,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
nav_tabs << :operations
end
if project.external_issue_tracker
......
......@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
only: [:id, :title, :description, :color],
methods: [:text_color]
)
end
end
......
......@@ -222,6 +222,7 @@ class Project < ActiveRecord::Base
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_many :prometheus_metrics
......
......@@ -14,11 +14,20 @@ module Ci
@subject.triggered_by?(@user)
end
condition(:branch_allows_maintainer_push) do
@subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
end
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
enable :update_build
enable :update_commit_status
end
end
end
......@@ -4,8 +4,16 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
condition(:branch_allows_maintainer_push) do
@subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
end
rule { protected_ref }.prevent :update_pipeline
rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
enable :update_pipeline
end
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
......
......@@ -77,7 +77,7 @@ class ProjectPolicy < BasePolicy
condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
condition(:has_merge_requests_allowing_pushes, scope: :subject) do
condition(:has_merge_requests_allowing_pushes) do
project.merge_requests_allowing_push_to_user(user).any?
end
......@@ -355,9 +355,7 @@ class ProjectPolicy < BasePolicy
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build
enable :update_build
enable :create_pipeline
enable :update_pipeline
end
rule do
......
......@@ -130,6 +130,7 @@ class SystemHooksService
{
group_name: model.group.name,
group_path: model.group.path,
group_plan: model.group.plan&.name,
group_id: model.group.id,
user_username: model.user.username,
user_name: model.user.name,
......
......@@ -13,7 +13,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
= number_with_delimiter(Project.cached_count)
= approximate_count_with_delimiters(Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
......@@ -22,7 +22,7 @@
= link_to admin_users_path do
%h3.text-center
Users:
= number_with_delimiter(User.count)
= approximate_count_with_delimiters(User)
-# EE specific
.text-center
= link_to 'Users statistics', admin_dashboard_stats_path
......@@ -34,7 +34,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
= number_with_delimiter(Group.count)
= approximate_count_with_delimiters(Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
......@@ -45,31 +45,31 @@
%p
Forks
%span.light.pull-right
= number_with_delimiter(ForkedProjectLink.count)
= approximate_count_with_delimiters(ForkedProjectLink)
%p
Issues
%span.light.pull-right
= number_with_delimiter(Issue.count)
= approximate_count_with_delimiters(Issue)
%p
Merge Requests
%span.light.pull-right
= number_with_delimiter(MergeRequest.count)
= approximate_count_with_delimiters(MergeRequest)
%p
Notes
%span.light.pull-right
= number_with_delimiter(Note.count)
= approximate_count_with_delimiters(Note)
%p
Snippets
%span.light.pull-right
= number_with_delimiter(Snippet.count)
= approximate_count_with_delimiters(Snippet)
%p
SSH Keys
%span.light.pull-right
= number_with_delimiter(Key.count)
= approximate_count_with_delimiters(Key)
%p
Milestones
%span.light.pull-right
= number_with_delimiter(Milestone.count)
= approximate_count_with_delimiters(Milestone)
%p
Active Users
%span.light.pull-right
......
......@@ -13,7 +13,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
Toggle discussion
= _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
......
......@@ -35,7 +35,9 @@
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
- if cluster_ingress_ip = cluster_ingress_ip(@project)
= s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
......@@ -28,9 +28,16 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
= link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do
= _("Delete")
%button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-wiki-modal',
delete_wiki_url: project_wiki_path(@project, @page),
page_title: @page.title.capitalize },
id: 'delete-wiki-button',
type: 'button' }
= _('Delete')
= render 'form'
= render 'sidebar'
#delete-wiki-modal.modal.fade
......@@ -15,7 +15,7 @@
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" }
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
- if can?(current_user, :admin_list, current_board_parent)
......
......@@ -18,7 +18,7 @@
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
= render "shared/issuable/sidebar_item_epic", issuable: issuable
= render_sidebar_epic(issuable)
.block.milestone
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true')
......
......@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn
%button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
%button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' }
= sprite_icon('duplicate', size: 16)
.clearfix
---
title: Fix width of contributors graphs
merge_request: 18639
author: Paul Vorbach
type: fixed
---
title: made listing and showing public issue apis available without authentication
merge_request: 18638
author: haseebeqx
type: changed
---
title: Fix issue board bug with long strings in titles
merge_request: 18924
author:
type: fixed
---
title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
merge_request: 18941
author:
type: changed
---
title: Display help text below auto devops domain with nip.io domain name (#45561)
merge_request: 18496
author:
type: added
---
title: Add index on runner_type for ci_runners
merge_request: 18897
author:
type: performance
---
title: Disables RBAC on nginx-ingress
merge_request: 18947
author:
type: fixed
---
title: fixed copy to blipboard button in embed bar of snippets
merge_request: 18923
author: haseebeqx
type: fixed
---
title: Fix setting Gitlab metrics content types
merge_request:
author:
type: fixed
---
title: Fix filename matching when processing file or blob search results
merge_request:
author:
type: fixed
---
title: Allow maintainers to retry pipelines on forked projects (if allowed in merge
request)
merge_request:
author:
type: fixed
---
title: Move discussion actions to the right for small viewports
merge_request: 18476
author: George Tsiolis
type: changed
---
title: Remove docker pull prefix from registry clipboard feature
merge_request: 18933
author: Lars Greiss
type: changed
---
title: Add a unique and not null constraint on the project_features.project_id column
merge_request:
author:
type: fixed
---
title: New design for wiki page deletion confirmation
merge_request: 18712
author: Constance Okoghenun
type: added
---
title: Adding branches through the WebUI is handled by Gitaly
merge_request:
author:
type: other
---
title: Refs containting sha checks are done by Gitaly
merge_request:
author:
type: other
class AddIndexOnCiRunnersRunnerType < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_runners, :runner_type
end
def down
remove_index :ci_runners, :runner_type
end
end
class AddUniqueConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class ProjectFeature < ActiveRecord::Base
self.table_name = 'project_features'
include EachBatch
end
def up
remove_duplicates
add_concurrent_index :project_features, :project_id, unique: true, name: 'index_project_features_on_project_id_unique'
remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id'
rename_index :project_features, 'index_project_features_on_project_id_unique', 'index_project_features_on_project_id'
end
def down
rename_index :project_features, 'index_project_features_on_project_id', 'index_project_features_on_project_id_old'
add_concurrent_index :project_features, :project_id
remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id_old'
end
private
def remove_duplicates
features = ProjectFeature
.select('MAX(id) AS max, COUNT(id), project_id')
.group(:project_id)
.having('COUNT(id) > 1')
features.each do |feature|
ProjectFeature
.where(project_id: feature['project_id'])
.where('id <> ?', feature['max'])
.each_batch { |batch| batch.delete_all }
end
end
end
class AddNotNullConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
class ProjectFeature < ActiveRecord::Base
include EachBatch
self.table_name = 'project_features'
end
def up
ProjectFeature.where(project_id: nil).delete_all
change_column_null :project_features, :project_id, false
end
def down
change_column_null :project_features, :project_id, true
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180509091305) do
ActiveRecord::Schema.define(version: 20180512061621) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -599,6 +599,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
create_table "ci_sources_pipelines", force: :cascade do |t|
......@@ -1946,7 +1947,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do
add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
t.integer "wiki_access_level"
......@@ -1957,7 +1958,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do
t.integer "repository_access_level", default: 20, null: false
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
......
# NFS
You can view information and options set for each of the mounted NFS file
systems by running `sudo nfsstat -m`.
systems by running `nfsstat -m` and `cat /etc/fstab`.
## NFS Server features
......
......@@ -151,7 +151,8 @@ The next step is to configure a Runner so that it picks the pending jobs.
In GitLab, Runners run the jobs that you define in `.gitlab-ci.yml`. A Runner
can be a virtual machine, a VPS, a bare-metal machine, a docker container or
even a cluster of containers. GitLab and the Runners communicate through an API,
so the only requirement is that the Runner's machine has [Internet] access.
so the only requirement is that the Runner's machine has network access to the
GitLab server.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
......@@ -226,4 +227,3 @@ CI with various languages.
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
[pipeline]: ../pipelines.md
[internet]: https://about.gitlab.com/images/theinternet.png
......@@ -22,9 +22,9 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?)
- [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow.
- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occassions.
- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions.
- [ ] **Plan your implementation:**
- [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. Its a good idea to go through your plan with another engineer to refine it.
- [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it.
- [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
- [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
- [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
......@@ -56,7 +56,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] If you have multiple MR's then also smoke test against the final merge.
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
- [ ] Follow up on issues that came out of the review. Create isssues for discovered edge cases that should be covered in future iterations.
- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
---
......
......@@ -68,10 +68,10 @@ Often we need to provide data from haml to our Vue application. Let's store it i
You can use `mapState` to access state properties in the components.
### `actions.js`
An action is a playload of information to send data from our application to our store.
An action is a payload of information to send data from our application to our store.
An action is usually composed by a `type` and a `payload` and they describe what happened.
Enforcing that every change is described as an action lets us have a clear understanting of what is going on in the app.
Enforcing that every change is described as an action lets us have a clear understanding of what is going on in the app.
In this file, we will write the actions that will call the respective mutations:
......@@ -87,7 +87,7 @@ In this file, we will write the actions that will call the respective mutations:
export const fetchUsers = ({ state, dispatch }) => {
dispatch('requestUsers');
axios.get(state.endoint)
axios.get(state.endpoint)
.then(({ data }) => dispatch('receiveUsersSuccess', data))
.catch((error) => {
dispatch('receiveUsersError', error)
......@@ -102,7 +102,7 @@ In this file, we will write the actions that will call the respective mutations:
export const addUser = ({ state, dispatch }, user) => {
dispatch('requestAddUser');
axios.post(state.endoint, user)
axios.post(state.endpoint, user)
.then(({ data }) => dispatch('receiveAddUserSuccess', data))
.catch((error) => dispatch('receiveAddUserError', error));
}
......@@ -126,7 +126,7 @@ The component MUST only dispatch the `fetchNamespace` action. Actions namespaced
The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
By following this pattern we guarantee:
1. All aplications follow the same pattern, making it easier for anyone to maintain the code
1. All applications follow the same pattern, making it easier for anyone to maintain the code
1. All data in the application follows the same lifecycle pattern
1. Actions are contained and human friendly
1. Unit tests are easier
......@@ -149,7 +149,7 @@ import { mapActions } from 'vuex';
};
```
#### `mutations.js`
### `mutations.js`
The mutations specify how the application state changes in response to actions sent to the store.
The only way to change state in a Vuex store should be by committing a mutation.
......@@ -175,7 +175,7 @@ Remember that actions only describe that something happened, they don't describe
state.isLoading = false;
},
[types.REQUEST_ADD_USER](state, user) {
state.isAddingUser = true;
state.isAddingUser = true;
},
[types.RECEIVE_ADD_USER_SUCCESS](state, user) {
state.isAddingUser = false;
......@@ -183,12 +183,12 @@ Remember that actions only describe that something happened, they don't describe
},
[types.REQUEST_ADD_USER_ERROR](state, error) {
state.isAddingUser = true;
state.errorAddingUser = error;
state.errorAddingUser = error;
},
};
```
#### `getters.js`
### `getters.js`
Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods)
This can be done through the `getters`:
......@@ -213,7 +213,7 @@ import { mapGetters } from 'vuex';
};
```
#### `mutations_types.js`
### `mutations_types.js`
From [vuex mutations docs][vuex-mutations]:
> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
......@@ -289,7 +289,7 @@ export default {
```
### Vuex Gotchas
1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs:
> why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
......@@ -342,7 +342,7 @@ describe('component', () => {
};
// populate the store
store.dipatch('addUser', user);
store.dispatch('addUser', user);
vm = new Component({
store,
......@@ -352,6 +352,18 @@ describe('component', () => {
});
```
#### Testing Vuex actions and getters
Because we're currently using [`babel-plugin-rewire`](https://github.com/speedskater/babel-plugin-rewire), you may encounter the following error when testing your Vuex actions and getters:
`[vuex] actions should be function or object with "handler" function`
To prevent this error from happening, you need to export an empty function as `default`:
```
// getters.js or actions.js
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
```
[vuex-docs]: https://vuex.vuejs.org
[vuex-structure]: https://vuex.vuejs.org/en/structure.html
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
......
......@@ -2,7 +2,7 @@
QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests.
> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/helpers/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
......
......@@ -133,11 +133,24 @@ really fast since:
- gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped
Note that in some cases, you might have to add some `require_dependency 'foo'`
in your file under test since Rails autoloading is not available in these cases.
This shouldn't be a problem since explicitely listing dependencies should be
considered a good practice anyway.
`fast_spec_helper` also support autoloading classes that are located inside the
`lib/` directory. It means that as long as your class / module is using only
code from the `lib/` directory you will not need to explicitly load any
dependencies. `fast_spec_helper` also loads all ActiveSupport extensions,
including core extensions that are commonly used in the Rails environment.
Note that in some cases, you might still have to load some dependencies using
`require_dependency` when a code is using gems or a dependency is not located
in `lib/`.
For example, if you want to test your code that is calling the
`Gitlab::UntrustedRegexp` class, which under the hood uses `re2` library, you
should either add `require_dependency 're2'` to files in your library that
need `re2` gem, to make this requirement explicit, or you can add it to the
spec itself, but the former is preferred.
It takes around one second to load tests that are using `fast_spec_helper`
instead of 30+ seconds in case of a regular `spec_helper`.
### `let` variables
......
......@@ -75,7 +75,6 @@ Shared Runners on GitLab.com run in [autoscale mode] and powered by
Google Cloud Platform and DigitalOcean. Autoscaling means reduced
waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security.
They're free to use for public open source projects and limited to 2000 CI
minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/).
......@@ -90,6 +89,10 @@ ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1.
Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`),
**will be timed out after 3 hours**, regardless of the timeout configured in a
project. Check the issues [4010] and [4070] for the reference.
Below are the shared Runners settings.
| Setting | GitLab.com | Default |
......@@ -340,3 +343,5 @@ High Performance TCP/HTTP Load Balancer:
[mailgun]: https://www.mailgun.com/ "Mailgun website"
[sidekiq]: http://sidekiq.org/ "Sidekiq website"
[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners"
[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com"
# Bulk Editing
>**Note:**
- A permission level of `Reporter` or higher is required in order to manage
issues.
- A permission level of `Developer` or higher is required in order to manage
merge requests.
Fields across multiple issues or merge requests can be updated simutaneously by using the bulk edit feature.
>**Note:**
- Bulk editing of issues and merge requests is only available at the project level.
To access the feature, navigate to either the issue or merge request list for the project and click 'Edit Issues' or 'Edit Merge Requests'. This will cause a sidebar to be shown on the right-hand side of the screen, where the available, editable fields are displayed. Checkboxes will also appear to the left-hand side of each issue or merge request, ready to be selected.
Once all items have been selected, choose the appropriate fields and their values from the sidebar and click 'Update All' to apply these changes.
......@@ -162,3 +162,7 @@ or Bugzilla.
### Issue's API
Read through the [API documentation](../../../api/issues.md).
### Bulk editing issues
Find out about [bulk editing issues](../../project/bulk_editing.md).
......@@ -324,6 +324,10 @@ all your changes will be available to preview by anyone with the Review Apps lin
[Read more about Review Apps.](../../../ci/review_apps/index.md)
## Bulk editing merge requests
Find out about [bulk editing merge requests](../../project/bulk_editing.md).
## Tips
Here are some tips that will help you be more efficient with merge requests in
......
......@@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open.
Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png)
[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395
......@@ -13,15 +13,27 @@ and from merge requests.
![Open Web IDE](img/open_web_ide.png)
## Commit changes
## File finder
Changed files are shown on the right in the commit panel. All changes are
automatically staged. To commit your changes, add a commit message and click
the 'Commit Button'.
> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8.
The file finder allows you to quickly open files in the current branch by
searching. The file finder is launched using the keyboard shortcut `Command-p`,
`Control-p`, or `t` (when editor is not in focus). Type the filename or
file path fragments to start seeing results.
## Stage and commit changes
After making your changes, click the Commit button in the bottom left to
review the list of changed files. Click on each file to review the changes and
click the tick icon to stage the file.
Once you have staged some changes, you can add a commit message and commit the
staged changes. Unstaged changes will not be commited.
![Commit changes](img/commit_changes.png)
## Comparing changes
## Reviewing changes
Before you commit your changes, you can compare them with the previous commit
by switching to the review mode or selecting the file from the staged files
......@@ -30,4 +42,5 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
[ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/
import initShow from '~/pages/projects/issues/show';
import initSidebarBundle from 'ee/sidebar/sidebar_bundle';
import initRelatedIssues from 'ee/related_issues';
import UserCallout from '~/user_callout';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initSidebarBundle();
initRelatedIssues();
// eslint-disable-next-line no-new
new UserCallout({ className: 'js-epics-sidebar-callout' });
// eslint-disable-next-line no-new
new UserCallout({ className: 'js-weight-sidebar-callout' });
});
<script>
import { n__, s__, sprintf } from '~/locale';
import Flash from '~/flash';
import MRWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
export default {
name: 'approvals-body',
name: 'ApprovalsBody',
components: {
MrWidgetAuthor,
},
props: {
mr: {
type: Object,
......@@ -17,27 +21,29 @@ export default {
approvedBy: {
type: Array,
required: false,
default: () => [],
},
approvalsLeft: {
type: Number,
required: false,
default: 0,
},
userCanApprove: {
type: Boolean,
required: false,
default: false,
},
userHasApproved: {
type: Boolean,
required: false,
default: false,
},
suggestedApprovers: {
type: Array,
required: false,
default: () => [],
},
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
data() {
return {
approving: false,
......@@ -102,32 +108,39 @@ export default {
});
},
},
template: `
<div class="approvals-body space-children">
<span v-if="showApproveButton" class="approvals-approve-button-wrap">
<button
:disabled="approving"
@click="approveMergeRequest"
class="btn btn-primary btn-sm approve-btn"
:class="approveButtonClass">
<i
v-if="approving"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
{{approveButtonText}}
</button>
</span>
<span class="approvals-required-text bold">
{{approvalsRequiredStringified}}
<span v-if="showSuggestedApprovers">
<mr-widget-author
v-for="approver in suggestedApprovers"
:key="approver.username"
:author="approver"
:show-author-name="false"
:show-author-tooltip="true" />
</span>
</span>
</div>
`,
};
</script>
<template>
<div class="approvals-body space-children">
<span
v-if="showApproveButton"
class="approvals-approve-button-wrap"
>
<button
:disabled="approving"
@click="approveMergeRequest"
class="btn btn-primary btn-sm approve-btn"
:class="approveButtonClass"
>
<i
v-if="approving"
class="fa fa-spinner fa-spin"
aria-hidden="true"
></i>
{{ approveButtonText }}
</button>
</span>
<span class="approvals-required-text bold">
{{ approvalsRequiredStringified }}
<span v-if="showSuggestedApprovers">
<mr-widget-author
v-for="approver in suggestedApprovers"
:key="approver.username"
:author="approver"
:show-author-name="false"
:show-author-tooltip="true"
/>
</span>
</span>
</div>
</template>
<script>
import Flash from '~/flash';
import LinkToMemberAvatar from 'ee/vue_shared/components/link_to_member_avatar';
import LinkToMemberAvatar from 'ee/vue_shared/components/link_to_member_avatar.vue';
import { s__ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
export default {
name: 'approvals-footer',
name: 'ApprovalsFooter',
components: {
LinkToMemberAvatar,
},
props: {
mr: {
type: Object,
......@@ -17,22 +21,27 @@ export default {
approvedBy: {
type: Array,
required: false,
default: () => [],
},
approvalsLeft: {
type: Number,
required: false,
default: 0,
},
userCanApprove: {
type: Boolean,
required: false,
default: false,
},
userHasApproved: {
type: Boolean,
required: false,
default: false,
},
suggestedApprovers: {
type: Array,
required: false,
default: () => [],
},
},
data() {
......@@ -40,9 +49,6 @@ export default {
unapproving: false,
};
},
components: {
'link-to-member-avatar': LinkToMemberAvatar,
},
computed: {
showUnapproveButton() {
const isMerged = this.mr.state === 'merged';
......@@ -58,8 +64,9 @@ export default {
methods: {
unapproveMergeRequest() {
this.unapproving = true;
this.service.unapproveMergeRequest()
.then((data) => {
this.service
.unapproveMergeRequest()
.then(data => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
this.unapproving = false;
......@@ -70,45 +77,49 @@ export default {
});
},
},
template: `
<div
v-if="approvedBy.length"
class="approved-by-users approvals-footer clearfix mr-info-list">
<div class="approvers-prefix">
<p>{{approvedByText}}</p>
<div class="approvers-list">
<link-to-member-avatar
v-for="(approver, index) in approvedBy"
:key="index"
:avatar-size="20"
:avatar-url="approver.user.avatar_url"
extra-link-class="approver-avatar js-approver-list-member"
:display-name="approver.user.name"
:profile-url="approver.user.web_url"
:show-tooltip="true"
/>
<link-to-member-avatar
v-for="n in approvalsLeft"
:key="n"
:avatar-size="20"
:clickable="false"
:show-tooltip="false"
/>
</div>
<button
v-if="showUnapproveButton"
type="button"
:disabled="unapproving"
@click="unapproveMergeRequest"
class="btn btn-sm unapprove-btn-wrap">
<i
v-if="unapproving"
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
{{removeApprovalText}}
</button>
};
</script>
<template>
<div
v-if="approvedBy.length"
class="approved-by-users approvals-footer clearfix mr-info-list"
>
<div class="approvers-prefix">
<p>{{ approvedByText }}</p>
<div class="approvers-list">
<link-to-member-avatar
v-for="(approver, index) in approvedBy"
:key="index"
:avatar-size="20"
:avatar-url="approver.user.avatar_url"
extra-link-class="approver-avatar js-approver-list-member"
:display-name="approver.user.name"
:profile-url="approver.user.web_url"
:show-tooltip="true"
/>
<link-to-member-avatar
v-for="n in approvalsLeft"
:key="n"
:avatar-size="20"
:clickable="false"
:show-tooltip="false"
/>
</div>
<button
v-if="showUnapproveButton"
type="button"
:disabled="unapproving"
@click="unapproveMergeRequest"
class="btn btn-sm unapprove-btn-wrap"
>
<i
v-if="unapproving"
class="fa fa-spinner fa-spin"
aria-hidden="true"
>
</i>
{{ removeApprovalText }}
</button>
</div>
`,
};
</div>
</template>
<script>
import Flash from '~/flash';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import { s__ } from '~/locale';
import ApprovalsBody from './approvals_body';
import ApprovalsFooter from './approvals_footer';
import ApprovalsBody from './approvals_body.vue';
import ApprovalsFooter from './approvals_footer.vue';
export default {
name: 'MRWidgetApprovals',
components: {
ApprovalsBody,
ApprovalsFooter,
statusIcon,
},
props: {
mr: {
type: Object,
......@@ -21,11 +27,7 @@ export default {
fetchingApprovals: true,
};
},
components: {
'approvals-body': ApprovalsBody,
'approvals-footer': ApprovalsFooter,
statusIcon,
},
computed: {
status() {
if (this.mr.approvals.approvals_left > 0) {
......@@ -35,51 +37,64 @@ export default {
},
},
created() {
const flashErrorMessage = s__('mrWidget|An error occured while retrieving approval data for this merge request.');
const flashErrorMessage = s__(
'mrWidget|An error occured while retrieving approval data for this merge request.',
);
this.service.fetchApprovals()
.then((data) => {
this.service
.fetchApprovals()
.then(data => {
this.mr.setApprovals(data);
this.fetchingApprovals = false;
})
.catch(() => new Flash(flashErrorMessage));
},
template: `
};
</script>
<template>
<div
v-if="mr.approvalsRequired"
class="mr-widget-approvals-container mr-widget-body mr-widget-section media"
>
<div
v-if="mr.approvalsRequired"
class="mr-widget-approvals-container mr-widget-body mr-widget-section media">
<div
v-if="fetchingApprovals"
class="mr-widget-icon">
<i class="fa fa-spinner fa-spin" />
</div>
<status-icon v-else :status="status" />
<div
v-show="fetchingApprovals"
class="mr-approvals-loading-state media-body">
<span class="approvals-loading-text">
Checking approval status
</span>
</div>
<div
v-if="!fetchingApprovals"
class="approvals-components media-body">
<approvals-body
:mr="mr"
:service="service"
:user-can-approve="mr.approvals.user_can_approve"
:user-has-approved="mr.approvals.user_has_approved"
:approved-by="mr.approvals.approved_by"
:approvals-left="mr.approvals.approvals_left"
:suggested-approvers="mr.approvals.suggested_approvers" />
<approvals-footer
:mr="mr"
:service="service"
:user-can-approve="mr.approvals.user_can_approve"
:user-has-approved="mr.approvals.user_has_approved"
:approved-by="mr.approvals.approved_by"
:approvals-left="mr.approvals.approvals_left" />
</div>
v-if="fetchingApprovals"
class="mr-widget-icon"
>
<i class="fa fa-spinner fa-spin"></i>
</div>
`,
};
<status-icon
v-else
:status="status"
/>
<div
v-show="fetchingApprovals"
class="mr-approvals-loading-state media-body"
>
<span class="approvals-loading-text">
Checking approval status
</span>
</div>
<div
v-if="!fetchingApprovals"
class="approvals-components media-body"
>
<approvals-body
:mr="mr"
:service="service"
:user-can-approve="mr.approvals.user_can_approve"
:user-has-approved="mr.approvals.user_has_approved"
:approved-by="mr.approvals.approved_by"
:approvals-left="mr.approvals.approvals_left"
:suggested-approvers="mr.approvals.suggested_approvers"
/>
<approvals-footer
:mr="mr"
:service="service"
:user-can-approve="mr.approvals.user_can_approve"
:user-has-approved="mr.approvals.user_has_approved"
:approved-by="mr.approvals.approved_by"
:approvals-left="mr.approvals.approvals_left"
/>
</div>
</div>
</template>
<script>
/**
* Renders Code quality body text
* Fixed: [name] in [link]:[line]
*/
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
/**
* Renders Code quality body text
* Fixed: [name] in [link]:[line]
*/
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
export default {
name: 'CodequalityIssueBody',
export default {
name: 'CodequalityIssueBody',
components: {
ReportLink,
},
components: {
ReportLink,
},
props: {
isStatusSuccess: {
type: Boolean,
required: true,
},
issue: {
type: Object,
required: true,
},
props: {
isStatusSuccess: {
type: Boolean,
required: true,
},
issue: {
type: Object,
required: true,
},
};
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
/**
* Renders Perfomance issue body text
* [name] :[score] [symbol] [delta] in [link]
*/
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
/**
* Renders Perfomance issue body text
* [name] :[score] [symbol] [delta] in [link]
*/
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
export default {
name: 'PerformanceIssueBody',
export default {
name: 'PerformanceIssueBody',
components: {
ReportLink,
},
components: {
ReportLink,
},
props: {
issue: {
type: Object,
required: true,
},
props: {
issue: {
type: Object,
required: true,
},
},
methods: {
formatScore(value) {
if (Math.floor(value) !== value) {
return parseFloat(value).toFixed(2);
}
return value;
},
methods: {
formatScore(value) {
if (Math.floor(value) !== value) {
return parseFloat(value).toFixed(2);
}
return value;
},
};
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
import eventHub from '~/vue_merge_request_widget/event_hub';
import ReadyToMergeState from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
export default {
extends: ReadyToMergeState,
name: 'ReadyToMerge',
components: {
'squash-before-merge': SquashBeforeMerge,
SquashBeforeMerge,
},
extends: ReadyToMergeState,
data() {
return {
additionalParams: {
......@@ -15,6 +16,11 @@ export default {
},
};
},
created() {
eventHub.$on('MRWidgetUpdateSquash', val => {
this.additionalParams.squash = val;
});
},
methods: {
// called in CE super component before form submission
setAdditionalParams(options) {
......@@ -23,9 +29,5 @@ export default {
}
},
},
created() {
eventHub.$on('MRWidgetUpdateSquash', (val) => {
this.additionalParams.squash = val;
});
},
};
</script>
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
export default {
props: {
mr: {
type: Object,
required: true,
},
},
components: {
statusIcon,
},
template: `
<div class="media">
<status-icon status="warning" showDisabledButton />
<div class="media-body">
<span class="bold">
Merge requests are read-only in a secondary Geo node
</span>
<a
:href="mr.geoSecondaryHelpPath"
data-title="About this feature"
data-toggle="tooltip"
data-placement="bottom"
target="_blank"
rel="noopener noreferrer nofollow"
data-container="body">
<i class="fa fa-question-circle"></i>
</a>
</div>
</div>
`,
};
<script>
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
export default {
components: {
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="media">
<status-icon
status="warning"
show-disabled-button
/>
<div class="media-body">
<span class="bold">
Merge requests are read-only in a secondary Geo node
</span>
<a
:href="mr.geoSecondaryHelpPath"
data-title="About this feature"
data-toggle="tooltip"
data-placement="bottom"
target="_blank"
rel="noopener noreferrer nofollow"
data-container="body"
>
<i class="fa fa-question-circle"></i>
</a>
</div>
</div>
</template>
......@@ -51,7 +51,8 @@ export default {
>
<i
class="fa fa-question-circle"
aria-hidden="true">
aria-hidden="true"
>
</i>
</a>
</div>
......
<script>
import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import WidgetApprovals from './components/approvals/mr_widget_approvals.vue';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import ReportSection from '../vue_shared/security_reports/components/report_section.vue';
import GroupedSecurityReportsApp from '../vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin';
......
......@@ -10,22 +10,19 @@ export default class MRWidgetService extends CEWidgetService {
}
fetchApprovals() {
return axios.get(this.approvalsPath)
.then(res => res.data);
return axios.get(this.approvalsPath).then(res => res.data);
}
approveMergeRequest() {
return axios.post(this.approvalsPath)
.then(res => res.data);
return axios.post(this.approvalsPath).then(res => res.data);
}
unapproveMergeRequest() {
return axios.delete(this.approvalsPath)
.then(res => res.data);
return axios.delete(this.approvalsPath).then(res => res.data);
}
fetchReport(endpoint) { // eslint-disable-line
return axios.get(endpoint)
.then(res => res.data);
// eslint-disable-next-line class-methods-use-this
fetchReport(endpoint) {
return axios.get(endpoint).then(res => res.data);
}
}
import CEGetStateKey from '~/vue_merge_request_widget/stores/get_state_key';
export default function (data) {
export default function(data) {
if (this.isGeoSecondaryNode) {
return 'geoSecondaryNode';
}
return CEGetStateKey.call(this, data);
}
<script>
// Analogue of link_to_member_avatar in app/helpers/projects_helper.rb
import pendingAvatarSvg from 'ee_icons/_icon_dotted_circle.svg';
......@@ -6,6 +7,7 @@ export default {
avatarUrl: {
type: String,
required: false,
default: '',
},
profileUrl: {
type: String,
......@@ -15,6 +17,7 @@ export default {
displayName: {
type: String,
required: false,
default: '',
},
extraAvatarClass: {
type: String,
......@@ -39,10 +42,7 @@ export default {
tooltipContainer: {
type: String,
required: false,
},
avatarHtml: {
type: String,
required: false,
default: 'body',
},
avatarSize: {
type: Number,
......@@ -75,32 +75,33 @@ export default {
linkClass() {
return `author_link ${this.tooltipClass} ${this.extraLinkClass} ${this.disabledClass}`;
},
tooltipContainerAttr() {
return this.tooltipContainer || 'body';
},
},
template: `
<div class="link-to-member-avatar">
<a
:href="profileUrl"
:class="linkClass"
:title="displayName"
:data-container="tooltipContainerAttr">
<img
v-if="avatarUrl"
:class="avatarClass"
:src="avatarUrl"
:width="avatarSize"
:height="avatarSize"
:alt="displayName"/>
<span
v-else
v-html="pendingAvatarSvg"
:class="avatarHtmlClass"
:width="avatarSize"
:height="avatarSize">
</span>
</a>
</div>
`,
};
</script>
<template>
<div class="link-to-member-avatar">
<a
:href="profileUrl"
:class="linkClass"
:title="displayName"
:data-container="tooltipContainer"
>
<img
v-if="avatarUrl"
:class="avatarClass"
:src="avatarUrl"
:width="avatarSize"
:height="avatarSize"
:alt="displayName"
/>
<span
v-else
v-html="pendingAvatarSvg"
:class="avatarHtmlClass"
:width="avatarSize"
:height="avatarSize"
>
</span>
</a>
</div>
</template>
......@@ -22,7 +22,6 @@
align-items: flex-start;
.close {
.dismiss-icon {
color: $gray-darkest;
}
......@@ -32,13 +31,11 @@
color: $text-color;
}
}
}
.svg-container {
margin-right: 15px;
}
}
&.promotion-empty-page {
......@@ -70,7 +67,6 @@
}
.promotion-modal {
.modal-dialog {
width: 540px;
}
......@@ -87,7 +83,6 @@
.modal-footer {
border-top: 0;
}
}
.promotion-backdrop {
......@@ -100,11 +95,17 @@
}
}
.promotion-info-weight-message {
padding: $gl-padding-top;
.promotion-issue-sidebar {
.promotion-issue-sidebar-message {
padding: $gl-padding-top;
.dropdown-title {
margin: 0 0 10px;
.dropdown-title {
margin: 0 0 10px;
}
.btn + .btn {
margin-top: $gl-padding-4;
}
}
.btn {
......@@ -114,7 +115,7 @@
line-height: $line-height-base;
}
.btn-link {
.right-sidebar & .btn-link {
display: inline;
color: $gl-link-color;
background-color: transparent;
......@@ -127,7 +128,6 @@
}
}
.promotion-issue-template-message {
padding: $gl-padding 30px 20px $gl-padding;
......@@ -160,4 +160,3 @@
}
}
}
module EE
module IssuablesHelper
extend ::Gitlab::Utils::Override
override :issuable_sidebar_options
def issuable_sidebar_options(issuable, can_edit_issuable)
super.merge(
weightOptions: ::Issue.weight_options,
weightNoneValue: ::Issue::WEIGHT_NONE
)
end
def render_sidebar_epic(issuable)
if issuable.project.feature_available?(:epics)
render 'shared/issuable/sidebar_item_epic', issuable: issuable
else
render 'shared/promotions/promote_epics'
end
end
end
end
......@@ -51,12 +51,10 @@ module EE
if succeeded
all_projects.each do |project|
old_path_with_namespace = File.join(full_path_was, project.path)
::Geo::RepositoryRenamedEventStore.new(
project,
old_path: project.path,
old_path_with_namespace: old_path_with_namespace
old_path_with_namespace: old_path_with_namespace_for(project)
).create
end
end
......@@ -64,6 +62,10 @@ module EE
succeeded
end
def old_path_with_namespace_for(project)
project.full_path.sub(/\A#{Regexp.escape(full_path)}/, full_path_was)
end
# Checks features (i.e. https://about.gitlab.com/products/) availabily
# for a given Namespace plan. This method should consider ancestor groups
# being licensed.
......
- return unless issuable.project.group&.feature_available?(:epics)
- if issuable.is_a?(Issue)
.block.epic
#js-vue-sidebar-item-epic
......
- promotion_feature = 'promote_epics_sidebar_dismissed'
- if !@project.feature_available?(:epics) && show_promotions? && show_callout?(promotion_feature)
.block.js-epics-sidebar-callout.promotion-issue-sidebar{ data: { uid: promotion_feature } }
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".js-epics-sidebar-callout" } }
%span{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: _('Epic') }
= sprite_icon('epic', size: 16)
%span
= _('None')
.title.hide-collapsed
= _('Epic')
.dropdown
.dropdown-menu.promotion-issue-sidebar-message
.dropdown-title
%span
= _('Epic')
%button.dropdown-title-button.dropdown-menu-close{ "aria-label" => _('Close'), :type => "button" }
%i.fa.fa-times.dropdown-menu-close-icon{ "aria-hidden" => "true", "data-hidden" => "true" }
%div
%p
= s_('Promotions|Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones.')
= link_to s_('Read more'), 'https://docs.gitlab.com/ee/user/group/epics/', class: 'btn-link', target: '_blank'
%div
= render 'shared/promotions/promotion_link_project', short_form: true
= link_to s_("Promotions|Don't show me this again"), '#', class: ['btn', 'js-close', 'js-close-callout']
.hide-collapsed
= s_('Promotions|This feature is locked.')
= link_to s_('Promotions|Upgrade plan') , '#', class: 'btn-link', data: { toggle: "dropdown", target: ".js-epics-sidebar-callout" }
- if show_promotions? && !@project.feature_available?(:issue_weights)
.block.weight
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".weight" } }
- promotion_feature = 'promote_weight_sidebar_dismissed'
- if !@project.feature_available?(:issue_weights) && show_promotions? && show_callout?(promotion_feature)
.block.js-weight-sidebar-callout.promotion-issue-sidebar{ data: { uid: promotion_feature } }
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".js-weight-sidebar-callout" } }
%span{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: _("Weight") }
= icon('balance-scale')
%span No
.title.hide-collapsed
= _('Weight')
= link_to _('Edit'), '#', class: 'edit-link promote-weight-link pull-right', data: { toggle: "dropdown", target: ".weight" }
.promotion-info-weight.dropdown
.dropdown-menu.promotion-info-weight-message
.dropdown
.dropdown-menu.promotion-issue-sidebar-message
.dropdown-title
%span
= _('Change Weight')
......@@ -23,6 +24,7 @@
= link_to _('Read more'), help_page_path('workflow/issue_weight.html'), class: 'btn-link', target: '_blank'
%div
= render 'shared/promotions/promotion_link_project', short_form: true
= link_to s_("Promotions|Don't show me this again"), '#', class: ['btn', 'js-close', 'js-close-callout']
.hide-collapsed
%span.no-value
= _('None')
= s_('Promotions|This feature is locked.')
= link_to s_('Promotions|Upgrade plan') , '#', class: 'btn-link', data: { toggle: "dropdown", target: ".js-weight-sidebar-callout" }
---
title: Add promotion for epics to issuable sidebar
merge_request: 5601
author:
type: other
---
title: Make issue weight promotion in issuable sidebar dismissable
merge_request: 5601
author:
type: changed
---
title: Use less aggressive sticking for DB load balancing
merge_request:
author:
type: performance
---
title: 'Geo: Fix repo, wiki, and upload replication when renaming a namespace that has subgroups'
merge_request: 5704
author:
type: fixed
......@@ -16,7 +16,6 @@ module Gitlab
delete
delete_all
insert
transaction
update
update_all
).freeze
......@@ -46,6 +45,19 @@ module Gitlab
end
end
def transaction(*args, &block)
Session.current.enter_transaction
write_using_load_balancer(:transaction, args, sticky: true, &block)
ensure
Session.current.leave_transaction
# When the transaction finishes we need to store the last WAL pointer
# since individual writes in a transaction don't perform this
# operation.
record_last_write_location
end
# Delegates all unknown messages to a read-write connection.
def method_missing(name, *args, &block)
write_using_load_balancer(name, args, &block)
......@@ -55,9 +67,7 @@ module Gitlab
#
# name - The name of the method to call on a connection object.
def read_using_load_balancer(name, args, &block)
method = Session.current.use_primary? ? :read_write : :read
@load_balancer.send(method) do |connection|
@load_balancer.send(load_balancer_method_for_read) do |connection|
connection.send(name, *args, &block)
end
end
......@@ -77,8 +87,45 @@ module Gitlab
connection.send(name, *args, &block)
end
# We only want to record the last write location if we actually
# performed a write, and not for all queries sent to the primary.
record_last_write_location if sticky
result
end
# Returns the method to use for performing a read-only query.
def load_balancer_method_for_read
session = Session.current
return :read unless session.use_primary?
# If we are still inside an explicit transaction we _must_ send the
# queries to the primary.
return :read_write if session.in_transaction?
# If we are not in an explicit transaction we are free to return to
# using the secondaries once they are all in sync.
if @load_balancer.all_caught_up?(session.last_write_location)
session.reset!
:read
else
:read_write
end
end
def record_last_write_location
session = Session.current
# When we are in a transaction it's likely we will perform many
# writes. In this case it's pointless to keep retrieving and storing
# the WAL location, as we only care about the location once the
# transaction finishes.
return if session.in_transaction?
session.last_write_location = @load_balancer.primary_write_location
end
end
end
end
......
......@@ -9,6 +9,8 @@ module Gitlab
class Session
CACHE_KEY = :gitlab_load_balancer_session
attr_accessor :last_write_location
def self.current
RequestStore[CACHE_KEY] ||= new
end
......@@ -18,6 +20,13 @@ module Gitlab
end
def initialize
@transaction_nesting = 0
reset!
end
def reset!
@last_write_location = nil
@use_primary = false
@performed_write = false
end
......@@ -26,6 +35,18 @@ module Gitlab
@use_primary
end
def enter_transaction
@transaction_nesting += 1
end
def leave_transaction
@transaction_nesting -= 1
end
def in_transaction?
@transaction_nesting.positive?
end
def use_primary!
@use_primary = true
end
......
......@@ -18,6 +18,7 @@ describe 'Promotions', :js do
it 'should show no promotion at all' do
sign_in(user)
visit edit_project_path(project)
expect(page).not_to have_selector('#promote_service_desk')
end
end
......@@ -33,12 +34,14 @@ describe 'Promotions', :js do
it 'should have the contact admin line' do
sign_in(user)
visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Contact your Administrator to upgrade your license.'
end
it 'should have the start trial button' do
sign_in(admin)
visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Start GitLab Ultimate trial'
end
end
......@@ -58,11 +61,13 @@ describe 'Promotions', :js do
it 'should have the Upgrade your plan button' do
visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Upgrade your plan'
end
it 'should have the contact owner line' do
visit edit_project_path(otherproject)
expect(find('#promote_service_desk')).to have_content 'Contact owner'
end
end
......@@ -79,6 +84,7 @@ describe 'Promotions', :js do
it 'should appear in project edit page' do
visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Improve customer support with GitLab Service Desk.'
end
......@@ -108,6 +114,7 @@ describe 'Promotions', :js do
it 'should appear in project edit page' do
visit edit_project_path(project)
expect(find('#promote_mr_features')).to have_content 'Improve Merge Requests'
end
......@@ -165,6 +172,7 @@ describe 'Promotions', :js do
it 'should appear in new MR page' do
visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'feature' })
expect(find('#promote_squash_commits')).to have_content 'Improve Merge Requests with Squash Commit and GitLab Enterprise Edition.'
end
......@@ -192,6 +200,7 @@ describe 'Promotions', :js do
it 'should appear in milestone page' do
visit project_milestone_path(project, milestone)
expect(find('#promote_burndown_charts')).to have_content "Upgrade your plan to improve milestones with Burndown Charts."
end
......@@ -219,6 +228,7 @@ describe 'Promotions', :js do
it 'should appear in milestone page' do
visit project_boards_path(project)
expect(find('.board-promotion-state')).to have_content "Upgrade your plan to improve Issue boards"
end
......@@ -246,11 +256,64 @@ describe 'Promotions', :js do
it 'should appear on export modal' do
visit project_issues_path(project)
click_on 'Export as CSV'
expect(find('.issues-export-modal')).to have_content 'Export issues with GitLab Enterprise Edition.'
end
end
describe 'for epics in issues sidebar', :js do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
project.add_master(user)
sign_in(user)
end
it 'should appear on the page' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
expect(find('.promotion-issue-sidebar-message')).to have_content 'Epics let you manage your portfolio of projects more efficiently'
end
it 'should be removed after dismissal' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
find('.js-epics-sidebar-callout .js-close-callout').click
expect(page).not_to have_selector('.js-epics-sidebar-callout')
end
it 'should not appear on page after dismissal and reload' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
find('.js-epics-sidebar-callout .js-close-callout').click
visit project_issue_path(project, issue)
expect(page).not_to have_selector('.js-epics-sidebar-callout')
end
it 'should close dialog when clicking on X, but not dismiss it' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
find('.js-epics-sidebar-callout .dropdown-menu-close').click
expect(page).to have_selector('.js-epics-sidebar-callout')
expect(page).to have_selector('.promotion-issue-sidebar-message', visible: false)
end
end
describe 'for issue weight', :js do
before do
allow(License).to receive(:current).and_return(nil)
......@@ -263,8 +326,42 @@ describe 'Promotions', :js do
it 'should appear on the page', :js do
visit project_issue_path(project, issue)
wait_for_requests
find('.promote-weight-link').click
expect(find('.promotion-info-weight-message')).to have_content 'Improve issues management with Issue weight and GitLab Enterprise Edition'
find('.js-weight-sidebar-callout .btn-link').click
expect(find('.promotion-issue-sidebar-message')).to have_content 'Improve issues management with Issue weight and GitLab Enterprise Edition'
end
it 'should be removed after dismissal' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-weight-sidebar-callout .btn-link').click
find('.js-weight-sidebar-callout .js-close-callout').click
expect(page).not_to have_selector('.js-weight-sidebar-callout')
end
it 'should not appear on page after dismissal and reload' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-weight-sidebar-callout .btn-link').click
find('.js-weight-sidebar-callout .js-close-callout').click
visit project_issue_path(project, issue)
expect(page).not_to have_selector('.js-weight-sidebar-callout')
end
it 'should close dialog when clicking on X, but not dismiss it' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-weight-sidebar-callout .btn-link').click
find('.js-weight-sidebar-callout .dropdown-menu-close').click
expect(page).to have_selector('.js-weight-sidebar-callout')
expect(page).to have_selector('.promotion-issue-sidebar-message', visible: false)
end
end
......@@ -280,7 +377,9 @@ describe 'Promotions', :js do
it 'should appear on the page', :js do
visit new_project_issue_path(project)
wait_for_requests
find('#promotion-issue-template-link').click
expect(find('.promotion-issue-template-message')).to have_content 'Description templates allow you to define context-specific templates for issue and merge request description fields for your project.'
end
end
......@@ -296,6 +395,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do
visit project_audit_events_path(project)
expect(find('.user-callout-copy')).to have_content 'Track your project with Audit Events'
end
end
......@@ -311,6 +411,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do
visit group_analytics_path(group)
expect(find('.user-callout-copy')).to have_content 'Track activity with Contribution Analytics.'
end
end
......@@ -326,6 +427,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do
visit group_hooks_path(group)
expect(find('.user-callout-copy')).to have_content 'Add Group Webhooks'
end
end
......
......@@ -49,8 +49,16 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
# We have an extra test for #transaction here to make sure that nested queries
# are also sent to a primary.
describe '#transaction' do
after do
Gitlab::Database::LoadBalancing::Session.clear_session
let(:session) { Gitlab::Database::LoadBalancing::Session.new }
before do
allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session)
allow(proxy.load_balancer)
.to receive(:primary_write_location)
.and_return('123/ABC')
end
it 'runs the transaction and any nested queries on the primary' do
......@@ -60,15 +68,36 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
allow(primary).to receive(:select)
expect(proxy.load_balancer).to receive(:read_write)
.twice.and_yield(primary)
.twice
.and_yield(primary)
# This expectation is put in place to ensure no read is performed.
expect(proxy.load_balancer).not_to receive(:read)
proxy.transaction { proxy.select('true') }
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?)
.to eq(true)
expect(session.use_primary?).to eq(true)
end
it 'tracks the state of the transaction in the session' do
expect(proxy)
.to receive(:write_using_load_balancer)
.with(:transaction, [10], { sticky: true })
expect(session).to receive(:enter_transaction)
expect(session).to receive(:leave_transaction)
proxy.transaction(10)
end
it 'records the last write location' do
allow(proxy)
.to receive(:write_using_load_balancer)
.with(:transaction, [10], { sticky: true })
proxy.transaction(10)
expect(session.last_write_location).to eq('123/ABC')
end
end
......@@ -91,62 +120,136 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
describe '#read_using_load_balancer' do
let(:session) { double(:session) }
let(:session) { Gitlab::Database::LoadBalancing::Session.new }
let(:connection) { double(:connection) }
before do
allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session)
end
describe 'with a regular session' do
it 'uses a secondary' do
allow(session).to receive(:use_primary?).and_return(false)
it 'performs a read-only query' do
allow(proxy.load_balancer)
.to receive(:load_balancer_method_for_read)
.and_return(:read)
expect(connection).to receive(:foo).with('foo')
expect(proxy.load_balancer).to receive(:read).and_yield(connection)
proxy.read_using_load_balancer(:foo, ['foo'])
end
end
allow(proxy.load_balancer)
.to receive(:read)
.and_yield(connection)
describe 'with a session using the primary' do
it 'uses the primary' do
allow(session).to receive(:use_primary?).and_return(true)
expect(connection)
.to receive(:foo)
.with('foo')
expect(connection).to receive(:foo).with('foo')
expect(proxy.load_balancer).to receive(:read_write)
.and_yield(connection)
proxy.read_using_load_balancer(:foo, ['foo'])
end
proxy.read_using_load_balancer(:foo, ['foo'])
end
end
describe '#write_using_load_balancer' do
let(:session) { double(:session) }
let(:session) { Gitlab::Database::LoadBalancing::Session.new }
let(:connection) { double(:connection) }
before do
allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session)
allow(proxy.load_balancer)
.to receive(:primary_write_location)
.and_return('123/ABC')
allow(proxy.load_balancer)
.to receive(:read_write)
.and_yield(connection)
allow(connection)
.to receive(:foo)
.with('foo')
end
it 'it uses but does not stick to the primary when sticking is disabled' do
expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
expect(connection).to receive(:foo).with('foo')
expect(session).not_to receive(:write!)
proxy.write_using_load_balancer(:foo, ['foo'])
end
it 'sticks to the primary when sticking is enabled' do
expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
expect(connection).to receive(:foo).with('foo')
expect(session).to receive(:write!)
proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
end
it 'tracks the last write location' do
proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
expect(session.last_write_location).to be_instance_of(String)
end
it 'does not track the last write location inside a transaction' do
session.enter_transaction
proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
expect(session.last_write_location).to be_nil
end
it 'does not track the last write location if sticking is not needed' do
proxy.write_using_load_balancer(:foo, ['foo'], sticky: false)
expect(session.last_write_location).to be_nil
end
end
describe '#load_balancer_method_for_read' do
let(:session) { Gitlab::Database::LoadBalancing::Session.new }
before do
allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session)
end
context 'when using the primary' do
before do
session.use_primary!
end
it 'returns :read_write when in a transaction' do
session.enter_transaction
expect(proxy.load_balancer_method_for_read).to eq(:read_write)
end
it 'returns :read_write if the secondaries are not in sync' do
session.last_write_location = '123/ABC'
allow(proxy.load_balancer)
.to receive(:all_caught_up?)
.with('123/ABC')
.and_return(false)
expect(proxy.load_balancer_method_for_read).to eq(:read_write)
end
it 'returns :read if all secondaries are in sync' do
session.last_write_location = '123/ABC'
allow(proxy.load_balancer)
.to receive(:all_caught_up?)
.with('123/ABC')
.and_return(true)
expect(proxy.load_balancer_method_for_read).to eq(:read)
expect(session.use_primary?).to eq(false)
end
end
context 'when using a secondary' do
it 'returns :read' do
expect(proxy.load_balancer_method_for_read).to eq(:read)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment