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: ...@@ -278,7 +278,6 @@ stages:
<<: *use-pg <<: *use-pg
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
CREATE_DB_USER: "true"
script: script:
# Manually clone gitlab-test and only seed this project in # Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below # db/fixtures/development/04_project.rb thanks to SIZE=1 below
...@@ -322,7 +321,7 @@ stages: ...@@ -322,7 +321,7 @@ stages:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables: variables:
CREATE_DB_USER: "true" SETUP_DB: "false"
script: script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee - git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
...@@ -331,13 +330,31 @@ stages: ...@@ -331,13 +330,31 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date - date
- git checkout $CI_COMMIT_SHA - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
- date - date
- . scripts/prepare_build.sh - . scripts/prepare_build.sh
- date - date
- bundle exec rake db:migrate - 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 # Trigger a package build in omnibus-gitlab repository
# #
...@@ -738,6 +755,14 @@ migration:path-mysql: ...@@ -738,6 +755,14 @@ migration:path-mysql:
<<: *migration-paths <<: *migration-paths
<<: *use-mysql <<: *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 .db-rollback: &db-rollback
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script: script:
......
...@@ -42,7 +42,9 @@ When removing columns, tables, indexes or other structures: ...@@ -42,7 +42,9 @@ When removing columns, tables, indexes or other structures:
- [ ] Has been reviewed by a Database specialist - [ ] 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 [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) - [ ] 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 - [ ] 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 - [ ] 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) - [ ] 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._ ...@@ -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) - [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) - [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) - [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) - [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc) - [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) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements) - [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
...@@ -212,9 +212,10 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable ...@@ -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 or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone. ~"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. 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 | | Label | Meaning | Estimate time to fix | Guidance |
...@@ -224,16 +225,7 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -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) | | | ~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 | | ~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 ### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
| 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.)
Severity labels help us clearly communicate the impact of a ~bug on users. 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. ...@@ -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. | | ~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. | | ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Specific Severity guidance #### Severity impact guidance
| Label | Security Impact | | Label | Security Impact | Availability / Performance Impact |
|-------|-------------------------------------------------------------------| |-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) | | ~S1 | >50% users impacted (possible company extinction level event) | |
| ~S2 | Multiple customers impacted (but not apocalyptic) | | ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A single customer impacted | | ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No customer impact, or expected impact within 30 days | | ~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") ### Label for community contributors (~"Accepting Merge Requests")
......
...@@ -42,8 +42,3 @@ THE SOFTWARE. ...@@ -42,8 +42,3 @@ THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the components are licensed under the original license provided by the owner of the
applicable component. 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. ...@@ -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 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 ## 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). 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 @@ ...@@ -179,7 +179,7 @@
role="row" role="row"
> >
<div <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" role="gridcell"
> >
<div> <div>
......
...@@ -32,17 +32,17 @@ export default { ...@@ -32,17 +32,17 @@ export default {
<template> <template>
<div :class="className"> <div :class="className">
{{ actionText }} {{ actionText }}
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
<template v-if="editedBy"> <template v-if="editedBy">
by {{ s__('ByAuthor|by') }}
<a <a
:href="editedBy.path" :href="editedBy.path"
class="js-vue-author author_link"> class="js-vue-author author_link">
{{ editedBy.name }} {{ editedBy.name }}
</a> </a>
</template> </template>
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom"
/>
</div> </div>
</template> </template>
...@@ -62,6 +62,21 @@ export default { ...@@ -62,6 +62,21 @@ export default {
<template> <template>
<div class="note-header-info"> <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"> <a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span> <span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light"> <span class="note-headline-light">
...@@ -95,20 +110,5 @@ export default { ...@@ -95,20 +110,5 @@ export default {
</i> </i>
</span> </span>
</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> </div>
</template> </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 'jquery';
import _ from 'underscore'; import _ from 'underscore';
...@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats'; ...@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; 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 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() { export const ContributorsGraph = (function() {
function ContributorsGraph() {} function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = { ContributorsGraph.prototype.MARGIN = {
top: 20, top: 20,
right: 20, right: 10,
bottom: 30, bottom: 30,
left: 50 left: 40
}; };
ContributorsGraph.prototype.x_domain = null; ContributorsGraph.prototype.x_domain = null;
...@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() { ...@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = []; 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) { ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data; return ContributorsGraph.prototype.x_domain = data;
}; };
...@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) { function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master'); const $parentElement = $('#contributors-master');
const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1; this.data = data1;
this.update_content = this.update_content.bind(this); 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.height = 200;
this.x = null; this.x = null;
this.y = null; this.y = null;
...@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
} }
ContributorsMasterGraph.prototype.process_dates = function(data) { ContributorsMasterGraph.prototype.process_dates = function(data) {
var dates; const dates = this.get_dates(data);
dates = this.get_dates(data);
this.parse_dates(data); this.parse_dates(data);
return ContributorsGraph.set_dates(dates); return ContributorsGraph.set_dates(dates);
}; };
...@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.parse_dates = function(data) { ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate; const parseDate = d3.timeParse("%Y-%m-%d");
parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) { return data.forEach(function(d) {
return d.date = parseDate(d.date); return d.date = parseDate(d.date);
}); });
...@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.create_svg = function() { 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) { ContributorsMasterGraph.prototype.create_area = function(x, y) {
...@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass); extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) { function ContributorsAuthorGraph(data1) {
const $parentElements = $('.person');
this.data = data1; this.data = data1;
// Don't split graph size in half for mobile devices. // Don't split graph size in half for mobile devices.
if ($(window).width() < 768) { if ($(window).width() < 790) {
this.width = $('.content').width() - 80; this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else { } else {
this.width = ($('.content').width() / 2) - 100; this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
} }
this.height = 200; this.height = 200;
this.x = null; this.x = null;
...@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) { ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) { return this.area = d3.area().x(function(d) {
var parseDate; const parseDate = d3.timeParse("%Y-%m-%d");
parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d)); return x(parseDate(d));
}).y0(this.height).y1((function(_this) { }).y0(this.height).y1((function(_this) {
return function(d) { return function(d) {
...@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
}; };
ContributorsAuthorGraph.prototype.create_svg = function() { ContributorsAuthorGraph.prototype.create_svg = function() {
var persons = document.querySelectorAll('.person'); const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1]; 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) { 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 $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import csrf from '~/lib/utils/csrf';
import Wikis from './wikis'; import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki'; import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode'; import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form'; import GLForm from '../../../gl_form';
import deleteWikiModal from './components/delete_wiki_modal.vue';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // 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 @@ ...@@ -28,11 +28,6 @@
isOpen: false, isOpen: false,
}; };
}, },
computed: {
clipboardText() {
return `docker pull ${this.repo.location}`;
},
},
methods: { methods: {
...mapActions([ ...mapActions([
'fetchRepos', 'fetchRepos',
...@@ -84,7 +79,7 @@ ...@@ -84,7 +79,7 @@
<clipboard-button <clipboard-button
v-if="repo.location" v-if="repo.location"
:text="clipboardText" :text="repo.location"
:title="repo.location" :title="repo.location"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
......
...@@ -56,10 +56,6 @@ ...@@ -56,10 +56,6 @@
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
}, },
clipboardText(text) {
return `docker pull ${text}`;
},
showError(message) { showError(message) {
Flash(errorMessages[message]); Flash(errorMessages[message]);
}, },
...@@ -89,7 +85,7 @@ ...@@ -89,7 +85,7 @@
<clipboard-button <clipboard-button
v-if="item.location" v-if="item.location"
:title="item.location" :title="item.location"
:text="clipboardText(item.location)" :text="item.location"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
</td> </td>
......
...@@ -26,7 +26,7 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic ...@@ -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 NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.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 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 ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
......
...@@ -210,3 +210,15 @@ ...@@ -210,3 +210,15 @@
margin-left: -$size; 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 @@ ...@@ -67,7 +67,8 @@
padding: 8px 40px; padding: 8px 40px;
} }
.embed-toggle { .embed-toggle,
.snippet-clipboard-btn {
height: 35px; height: 35px;
} }
} }
...@@ -329,6 +329,9 @@ ...@@ -329,6 +329,9 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow; box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none; list-style: none;
// as a fallback, hide overflow content so that dragging and dropping still works
overflow: hidden;
&:not(:last-child) { &:not(:last-child) {
margin-bottom: 5px; margin-bottom: 5px;
} }
...@@ -355,14 +358,13 @@ ...@@ -355,14 +358,13 @@
} }
.card-title { .card-title {
@include overflow-break-word();
margin: 0 30px 0 0; margin: 0 30px 0 0;
font-size: 1em; font-size: 1em;
line-height: inherit; line-height: inherit;
a { a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word;
word-break: break-word;
margin-right: 2px; margin-right: 2px;
} }
} }
...@@ -531,8 +533,8 @@ ...@@ -531,8 +533,8 @@
} }
.issuable-header-text { .issuable-header-text {
@include overflow-break-word();
padding-right: 35px; padding-right: 35px;
word-break: break-word;
> strong { > strong {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
......
...@@ -407,10 +407,6 @@ ul.notes { ...@@ -407,10 +407,6 @@ ul.notes {
.note-header { .note-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
} }
.note-header-info { .note-header-info {
...@@ -473,11 +469,6 @@ ul.notes { ...@@ -473,11 +469,6 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
}
.btn-group > .discussion-next-btn { .btn-group > .discussion-next-btn {
margin-left: -1px; margin-left: -1px;
} }
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
prepend ::EE::Admin::DashboardController prepend ::EE::Admin::DashboardController
include CountHelper
def index def index
@projects = Project.order_id_desc.without_deleted.with_route.limit(10) @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10) @users = User.order_id_desc.limit(10)
......
...@@ -183,4 +183,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -183,4 +183,8 @@ class Projects::PipelinesController < Projects::ApplicationController
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343 # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
end end
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
end end
...@@ -24,6 +24,15 @@ module AutoDevopsHelper ...@@ -24,6 +24,15 @@ module AutoDevopsHelper
end end
end end
def cluster_ingress_ip(project)
project
.cluster_ingresses
.where("external_ip is not null")
.limit(1)
.pluck(:external_ip)
.first
end
private private
def missing_auto_devops_domain?(project) 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 module IssuablesHelper
prepend EE::IssuablesHelper
include GitlabRoutingHelper include GitlabRoutingHelper
def sidebar_gutter_toggle_icon def sidebar_gutter_toggle_icon
...@@ -390,9 +392,7 @@ module IssuablesHelper ...@@ -390,9 +392,7 @@ module IssuablesHelper
editable: can_edit_issuable, editable: can_edit_issuable,
currentUser: UserSerializer.new.represent(current_user), currentUser: UserSerializer.new.represent(current_user),
rootPath: root_path, rootPath: root_path,
fullPath: @project.full_path, fullPath: @project.full_path
weightOptions: Issue.weight_options,
weightNoneValue: Issue::WEIGHT_NONE
} }
end end
......
...@@ -259,6 +259,7 @@ module ProjectsHelper ...@@ -259,6 +259,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project) if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines nav_tabs << :pipelines
nav_tabs << :operations
end end
if project.external_issue_tracker if project.external_issue_tracker
......
...@@ -31,7 +31,8 @@ class List < ActiveRecord::Base ...@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label) if options.key?(:label)
json[:label] = label.as_json( json[:label] = label.as_json(
project: board.project, project: board.project,
only: [:id, :title, :description, :color] only: [:id, :title, :description, :color],
methods: [:text_color]
) )
end end
end end
......
...@@ -222,6 +222,7 @@ class Project < ActiveRecord::Base ...@@ -222,6 +222,7 @@ class Project < ActiveRecord::Base
has_one :cluster_project, class_name: 'Clusters::Project' has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' 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 has_many :prometheus_metrics
......
...@@ -14,11 +14,20 @@ module Ci ...@@ -14,11 +14,20 @@ module Ci
@subject.triggered_by?(@user) @subject.triggered_by?(@user)
end end
condition(:branch_allows_maintainer_push) do
@subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
end
rule { protected_ref }.policy do rule { protected_ref }.policy do
prevent :update_build prevent :update_build
prevent :erase_build prevent :erase_build
end end
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build 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
end end
...@@ -4,8 +4,16 @@ module Ci ...@@ -4,8 +4,16 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) } 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 { 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) def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project) access = ::Gitlab::UserAccess.new(user, project: project)
......
...@@ -77,7 +77,7 @@ class ProjectPolicy < BasePolicy ...@@ -77,7 +77,7 @@ class ProjectPolicy < BasePolicy
condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled } condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user" 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? project.merge_requests_allowing_push_to_user(user).any?
end end
...@@ -355,9 +355,7 @@ class ProjectPolicy < BasePolicy ...@@ -355,9 +355,7 @@ class ProjectPolicy < BasePolicy
# to run pipelines for the branches they have access to. # to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build enable :create_build
enable :update_build
enable :create_pipeline enable :create_pipeline
enable :update_pipeline
end end
rule do rule do
......
...@@ -130,6 +130,7 @@ class SystemHooksService ...@@ -130,6 +130,7 @@ class SystemHooksService
{ {
group_name: model.group.name, group_name: model.group.name,
group_path: model.group.path, group_path: model.group.path,
group_plan: model.group.plan&.name,
group_id: model.group.id, group_id: model.group.id,
user_username: model.user.username, user_username: model.user.username,
user_name: model.user.name, user_name: model.user.name,
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= link_to admin_projects_path do = link_to admin_projects_path do
%h3.text-center %h3.text-center
Projects: Projects:
= number_with_delimiter(Project.cached_count) = approximate_count_with_delimiters(Project)
%hr %hr
= link_to('New project', new_project_path, class: "btn btn-new") = link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4 .col-sm-4
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= link_to admin_users_path do = link_to admin_users_path do
%h3.text-center %h3.text-center
Users: Users:
= number_with_delimiter(User.count) = approximate_count_with_delimiters(User)
-# EE specific -# EE specific
.text-center .text-center
= link_to 'Users statistics', admin_dashboard_stats_path = link_to 'Users statistics', admin_dashboard_stats_path
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
= link_to admin_groups_path do = link_to admin_groups_path do
%h3.text-center %h3.text-center
Groups: Groups:
= number_with_delimiter(Group.count) = approximate_count_with_delimiters(Group)
%hr %hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new" = link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row .row
...@@ -45,31 +45,31 @@ ...@@ -45,31 +45,31 @@
%p %p
Forks Forks
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(ForkedProjectLink.count) = approximate_count_with_delimiters(ForkedProjectLink)
%p %p
Issues Issues
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Issue.count) = approximate_count_with_delimiters(Issue)
%p %p
Merge Requests Merge Requests
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(MergeRequest.count) = approximate_count_with_delimiters(MergeRequest)
%p %p
Notes Notes
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Note.count) = approximate_count_with_delimiters(Note)
%p %p
Snippets Snippets
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Snippet.count) = approximate_count_with_delimiters(Snippet)
%p %p
SSH Keys SSH Keys
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Key.count) = approximate_count_with_delimiters(Key)
%p %p
Milestones Milestones
%span.light.pull-right %span.light.pull-right
= number_with_delimiter(Milestone.count) = approximate_count_with_delimiters(Milestone)
%p %p
Active Users Active Users
%span.light.pull-right %span.light.pull-right
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= icon("chevron-up") = icon("chevron-up")
- else - else
= icon("chevron-down") = icon("chevron-down")
Toggle discussion = _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false) = link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light .inline.discussion-headline-light
......
...@@ -35,7 +35,9 @@ ...@@ -35,7 +35,9 @@
= _('Domain') = _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com' = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block .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' = 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" = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
...@@ -28,9 +28,16 @@ ...@@ -28,9 +28,16 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do = link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history") = s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project) - 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 %button.btn.btn-danger{ data: { toggle: 'modal',
= _("Delete") 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 'form'
= render 'sidebar' = render 'sidebar'
#delete-wiki-modal.modal.fade
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
":title" => '(list.label ? list.label.description : "")', ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" }, data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text", 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 }} {{ list.title }}
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee .block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? = 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 .block.milestone
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true') = icon('clock-o', 'aria-hidden': 'true')
......
...@@ -45,6 +45,6 @@ ...@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share") %strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn .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) = sprite_icon('duplicate', size: 16)
.clearfix .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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -599,6 +599,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do ...@@ -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", ["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", ["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", ["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 add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
create_table "ci_sources_pipelines", force: :cascade do |t| create_table "ci_sources_pipelines", force: :cascade do |t|
...@@ -1946,7 +1947,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do ...@@ -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 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| 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 "merge_requests_access_level"
t.integer "issues_access_level" t.integer "issues_access_level"
t.integer "wiki_access_level" t.integer "wiki_access_level"
...@@ -1957,7 +1958,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do ...@@ -1957,7 +1958,7 @@ ActiveRecord::Schema.define(version: 20180509091305) do
t.integer "repository_access_level", default: 20, null: false t.integer "repository_access_level", default: 20, null: false
end 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| create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
......
# NFS # NFS
You can view information and options set for each of the mounted NFS file 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 ## NFS Server features
......
...@@ -151,7 +151,8 @@ The next step is to configure a Runner so that it picks the pending jobs. ...@@ -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 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 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, 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 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_. GitLab. If it serves all projects it's called a _Shared Runner_.
...@@ -226,4 +227,3 @@ CI with various languages. ...@@ -226,4 +227,3 @@ CI with various languages.
[enabled]: ../enable_or_disable_ci.md [enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages [stages]: ../yaml/README.md#stages
[pipeline]: ../pipelines.md [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 ...@@ -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?) - [ ] 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. - [ ] 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? - [ ] 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:** - [ ] **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. - [ ] **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. - [ ] **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. - [ ] **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 ...@@ -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. - [ ] 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 - [ ] 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 - [ ] 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 ...@@ -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. You can use `mapState` to access state properties in the components.
### `actions.js` ### `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. 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: 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: ...@@ -87,7 +87,7 @@ In this file, we will write the actions that will call the respective mutations:
export const fetchUsers = ({ state, dispatch }) => { export const fetchUsers = ({ state, dispatch }) => {
dispatch('requestUsers'); dispatch('requestUsers');
axios.get(state.endoint) axios.get(state.endpoint)
.then(({ data }) => dispatch('receiveUsersSuccess', data)) .then(({ data }) => dispatch('receiveUsersSuccess', data))
.catch((error) => { .catch((error) => {
dispatch('receiveUsersError', error) dispatch('receiveUsersError', error)
...@@ -102,7 +102,7 @@ In this file, we will write the actions that will call the respective mutations: ...@@ -102,7 +102,7 @@ In this file, we will write the actions that will call the respective mutations:
export const addUser = ({ state, dispatch }, user) => { export const addUser = ({ state, dispatch }, user) => {
dispatch('requestAddUser'); dispatch('requestAddUser');
axios.post(state.endoint, user) axios.post(state.endpoint, user)
.then(({ data }) => dispatch('receiveAddUserSuccess', data)) .then(({ data }) => dispatch('receiveAddUserSuccess', data))
.catch((error) => dispatch('receiveAddUserError', error)); .catch((error) => dispatch('receiveAddUserError', error));
} }
...@@ -126,7 +126,7 @@ The component MUST only dispatch the `fetchNamespace` action. Actions namespaced ...@@ -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` The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
By following this pattern we guarantee: 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. All data in the application follows the same lifecycle pattern
1. Actions are contained and human friendly 1. Actions are contained and human friendly
1. Unit tests are easier 1. Unit tests are easier
...@@ -149,7 +149,7 @@ import { mapActions } from 'vuex'; ...@@ -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 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. 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 ...@@ -175,7 +175,7 @@ Remember that actions only describe that something happened, they don't describe
state.isLoading = false; state.isLoading = false;
}, },
[types.REQUEST_ADD_USER](state, user) { [types.REQUEST_ADD_USER](state, user) {
state.isAddingUser = true; state.isAddingUser = true;
}, },
[types.RECEIVE_ADD_USER_SUCCESS](state, user) { [types.RECEIVE_ADD_USER_SUCCESS](state, user) {
state.isAddingUser = false; state.isAddingUser = false;
...@@ -183,12 +183,12 @@ Remember that actions only describe that something happened, they don't describe ...@@ -183,12 +183,12 @@ Remember that actions only describe that something happened, they don't describe
}, },
[types.REQUEST_ADD_USER_ERROR](state, error) { [types.REQUEST_ADD_USER_ERROR](state, error) {
state.isAddingUser = true; 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. 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) 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`: This can be done through the `getters`:
...@@ -213,7 +213,7 @@ import { mapGetters } from 'vuex'; ...@@ -213,7 +213,7 @@ import { mapGetters } from 'vuex';
}; };
``` ```
#### `mutations_types.js` ### `mutations_types.js`
From [vuex mutations docs][vuex-mutations]: 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. > 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 { ...@@ -289,7 +289,7 @@ export default {
``` ```
### Vuex Gotchas ### 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. > 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', () => { ...@@ -342,7 +342,7 @@ describe('component', () => {
}; };
// populate the store // populate the store
store.dipatch('addUser', user); store.dispatch('addUser', user);
vm = new Component({ vm = new Component({
store, store,
...@@ -352,6 +352,18 @@ describe('component', () => { ...@@ -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-docs]: https://vuex.vuejs.org
[vuex-structure]: https://vuex.vuejs.org/en/structure.html [vuex-structure]: https://vuex.vuejs.org/en/structure.html
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html [vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
......
...@@ -2,7 +2,7 @@ ...@@ -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. 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. 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: ...@@ -133,11 +133,24 @@ really fast since:
- gitlab-shell and Gitaly setup are skipped - gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped - Test repositories setup are skipped
Note that in some cases, you might have to add some `require_dependency 'foo'` `fast_spec_helper` also support autoloading classes that are located inside the
in your file under test since Rails autoloading is not available in these cases. `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
This shouldn't be a problem since explicitely listing dependencies should be dependencies. `fast_spec_helper` also loads all ActiveSupport extensions,
considered a good practice anyway. 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 ### `let` variables
......
...@@ -75,7 +75,6 @@ Shared Runners on GitLab.com run in [autoscale mode] and powered by ...@@ -75,7 +75,6 @@ Shared Runners on GitLab.com run in [autoscale mode] and powered by
Google Cloud Platform and DigitalOcean. Autoscaling means reduced Google Cloud Platform and DigitalOcean. Autoscaling means reduced
waiting times to spin up CI/CD jobs, and isolated VMs for each project, waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security. thus maximizing security.
They're free to use for public open source projects and limited to 2000 CI 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 minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/). [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 ...@@ -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 installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1. 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. Below are the shared Runners settings.
| Setting | GitLab.com | Default | | Setting | GitLab.com | Default |
...@@ -340,3 +343,5 @@ High Performance TCP/HTTP Load Balancer: ...@@ -340,3 +343,5 @@ High Performance TCP/HTTP Load Balancer:
[mailgun]: https://www.mailgun.com/ "Mailgun website" [mailgun]: https://www.mailgun.com/ "Mailgun website"
[sidekiq]: http://sidekiq.org/ "Sidekiq website" [sidekiq]: http://sidekiq.org/ "Sidekiq website"
[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer" [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. ...@@ -162,3 +162,7 @@ or Bugzilla.
### Issue's API ### Issue's API
Read through the [API documentation](../../../api/issues.md). 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 ...@@ -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) [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 ## Tips
Here are some tips that will help you be more efficient with merge requests in 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. ...@@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open.
Enable this functionality while creating a merge request: Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png) ![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. ...@@ -13,15 +13,27 @@ and from merge requests.
![Open Web IDE](img/open_web_ide.png) ![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 > [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8.
automatically staged. To commit your changes, add a commit message and click
the 'Commit Button'. 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) ![Commit changes](img/commit_changes.png)
## Comparing changes ## Reviewing changes
Before you commit your changes, you can compare them with the previous commit 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 by switching to the review mode or selecting the file from the staged files
...@@ -30,4 +42,5 @@ list. ...@@ -30,4 +42,5 @@ list.
An additional review mode is available when you open a merge request, which 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. 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/ [ee]: https://about.gitlab.com/pricing/
import initShow from '~/pages/projects/issues/show'; import initShow from '~/pages/projects/issues/show';
import initSidebarBundle from 'ee/sidebar/sidebar_bundle'; import initSidebarBundle from 'ee/sidebar/sidebar_bundle';
import initRelatedIssues from 'ee/related_issues'; import initRelatedIssues from 'ee/related_issues';
import UserCallout from '~/user_callout';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initShow(); initShow();
initSidebarBundle(); initSidebarBundle();
initRelatedIssues(); 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 { n__, s__, sprintf } from '~/locale';
import Flash from '~/flash'; 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'; import eventHub from '~/vue_merge_request_widget/event_hub';
export default { export default {
name: 'approvals-body', name: 'ApprovalsBody',
components: {
MrWidgetAuthor,
},
props: { props: {
mr: { mr: {
type: Object, type: Object,
...@@ -17,27 +21,29 @@ export default { ...@@ -17,27 +21,29 @@ export default {
approvedBy: { approvedBy: {
type: Array, type: Array,
required: false, required: false,
default: () => [],
}, },
approvalsLeft: { approvalsLeft: {
type: Number, type: Number,
required: false, required: false,
default: 0,
}, },
userCanApprove: { userCanApprove: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false,
}, },
userHasApproved: { userHasApproved: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false,
}, },
suggestedApprovers: { suggestedApprovers: {
type: Array, type: Array,
required: false, required: false,
default: () => [],
}, },
}, },
components: {
'mr-widget-author': MRWidgetAuthor,
},
data() { data() {
return { return {
approving: false, approving: false,
...@@ -102,32 +108,39 @@ export default { ...@@ -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 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 { s__ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
export default { export default {
name: 'approvals-footer', name: 'ApprovalsFooter',
components: {
LinkToMemberAvatar,
},
props: { props: {
mr: { mr: {
type: Object, type: Object,
...@@ -17,22 +21,27 @@ export default { ...@@ -17,22 +21,27 @@ export default {
approvedBy: { approvedBy: {
type: Array, type: Array,
required: false, required: false,
default: () => [],
}, },
approvalsLeft: { approvalsLeft: {
type: Number, type: Number,
required: false, required: false,
default: 0,
}, },
userCanApprove: { userCanApprove: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false,
}, },
userHasApproved: { userHasApproved: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false,
}, },
suggestedApprovers: { suggestedApprovers: {
type: Array, type: Array,
required: false, required: false,
default: () => [],
}, },
}, },
data() { data() {
...@@ -40,9 +49,6 @@ export default { ...@@ -40,9 +49,6 @@ export default {
unapproving: false, unapproving: false,
}; };
}, },
components: {
'link-to-member-avatar': LinkToMemberAvatar,
},
computed: { computed: {
showUnapproveButton() { showUnapproveButton() {
const isMerged = this.mr.state === 'merged'; const isMerged = this.mr.state === 'merged';
...@@ -58,8 +64,9 @@ export default { ...@@ -58,8 +64,9 @@ export default {
methods: { methods: {
unapproveMergeRequest() { unapproveMergeRequest() {
this.unapproving = true; this.unapproving = true;
this.service.unapproveMergeRequest() this.service
.then((data) => { .unapproveMergeRequest()
.then(data => {
this.mr.setApprovals(data); this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
this.unapproving = false; this.unapproving = false;
...@@ -70,45 +77,49 @@ export default { ...@@ -70,45 +77,49 @@ export default {
}); });
}, },
}, },
template: ` };
<div </script>
v-if="approvedBy.length" <template>
class="approved-by-users approvals-footer clearfix mr-info-list"> <div
<div class="approvers-prefix"> v-if="approvedBy.length"
<p>{{approvedByText}}</p> class="approved-by-users approvals-footer clearfix mr-info-list"
<div class="approvers-list"> >
<link-to-member-avatar <div class="approvers-prefix">
v-for="(approver, index) in approvedBy" <p>{{ approvedByText }}</p>
:key="index" <div class="approvers-list">
:avatar-size="20" <link-to-member-avatar
:avatar-url="approver.user.avatar_url" v-for="(approver, index) in approvedBy"
extra-link-class="approver-avatar js-approver-list-member" :key="index"
:display-name="approver.user.name" :avatar-size="20"
:profile-url="approver.user.web_url" :avatar-url="approver.user.avatar_url"
:show-tooltip="true" extra-link-class="approver-avatar js-approver-list-member"
/> :display-name="approver.user.name"
<link-to-member-avatar :profile-url="approver.user.web_url"
v-for="n in approvalsLeft" :show-tooltip="true"
:key="n" />
:avatar-size="20" <link-to-member-avatar
:clickable="false" v-for="n in approvalsLeft"
:show-tooltip="false" :key="n"
/> :avatar-size="20"
</div> :clickable="false"
<button :show-tooltip="false"
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>
<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>
`, </div>
}; </template>
<script>
import Flash from '~/flash'; import Flash from '~/flash';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ApprovalsBody from './approvals_body'; import ApprovalsBody from './approvals_body.vue';
import ApprovalsFooter from './approvals_footer'; import ApprovalsFooter from './approvals_footer.vue';
export default { export default {
name: 'MRWidgetApprovals', name: 'MRWidgetApprovals',
components: {
ApprovalsBody,
ApprovalsFooter,
statusIcon,
},
props: { props: {
mr: { mr: {
type: Object, type: Object,
...@@ -21,11 +27,7 @@ export default { ...@@ -21,11 +27,7 @@ export default {
fetchingApprovals: true, fetchingApprovals: true,
}; };
}, },
components: {
'approvals-body': ApprovalsBody,
'approvals-footer': ApprovalsFooter,
statusIcon,
},
computed: { computed: {
status() { status() {
if (this.mr.approvals.approvals_left > 0) { if (this.mr.approvals.approvals_left > 0) {
...@@ -35,51 +37,64 @@ export default { ...@@ -35,51 +37,64 @@ export default {
}, },
}, },
created() { 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() this.service
.then((data) => { .fetchApprovals()
.then(data => {
this.mr.setApprovals(data); this.mr.setApprovals(data);
this.fetchingApprovals = false; this.fetchingApprovals = false;
}) })
.catch(() => new Flash(flashErrorMessage)); .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 <div
v-if="mr.approvalsRequired" v-if="fetchingApprovals"
class="mr-widget-approvals-container mr-widget-body mr-widget-section media"> class="mr-widget-icon"
<div >
v-if="fetchingApprovals" <i class="fa fa-spinner fa-spin"></i>
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>
</div> </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> <script>
/** /**
* Renders Code quality body text * Renders Code quality body text
* Fixed: [name] in [link]:[line] * Fixed: [name] in [link]:[line]
*/ */
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue'; import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
export default { export default {
name: 'CodequalityIssueBody', name: 'CodequalityIssueBody',
components: { components: {
ReportLink, ReportLink,
}, },
props: { props: {
isStatusSuccess: { isStatusSuccess: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script> <script>
/** /**
* Renders Perfomance issue body text * Renders Perfomance issue body text
* [name] :[score] [symbol] [delta] in [link] * [name] :[score] [symbol] [delta] in [link]
*/ */
import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue'; import ReportLink from 'ee/vue_shared/security_reports/components/report_link.vue';
export default { export default {
name: 'PerformanceIssueBody', name: 'PerformanceIssueBody',
components: { components: {
ReportLink, ReportLink,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
}, },
},
methods: { methods: {
formatScore(value) { formatScore(value) {
if (Math.floor(value) !== value) { if (Math.floor(value) !== value) {
return parseFloat(value).toFixed(2); return parseFloat(value).toFixed(2);
} }
return value; return value;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import ReadyToMergeState from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import ReadyToMergeState from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import SquashBeforeMerge from './mr_widget_squash_before_merge.vue'; import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
export default { export default {
extends: ReadyToMergeState,
name: 'ReadyToMerge', name: 'ReadyToMerge',
components: { components: {
'squash-before-merge': SquashBeforeMerge, SquashBeforeMerge,
}, },
extends: ReadyToMergeState,
data() { data() {
return { return {
additionalParams: { additionalParams: {
...@@ -15,6 +16,11 @@ export default { ...@@ -15,6 +16,11 @@ export default {
}, },
}; };
}, },
created() {
eventHub.$on('MRWidgetUpdateSquash', val => {
this.additionalParams.squash = val;
});
},
methods: { methods: {
// called in CE super component before form submission // called in CE super component before form submission
setAdditionalParams(options) { setAdditionalParams(options) {
...@@ -23,9 +29,5 @@ export default { ...@@ -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 { ...@@ -51,7 +51,8 @@ export default {
> >
<i <i
class="fa fa-question-circle" class="fa fa-question-circle"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</a> </a>
</div> </div>
......
<script> <script>
import { n__, s__, __, sprintf } from '~/locale'; import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import WidgetApprovals from './components/approvals/mr_widget_approvals'; import WidgetApprovals from './components/approvals/mr_widget_approvals.vue';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node'; import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import ReportSection from '../vue_shared/security_reports/components/report_section.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 GroupedSecurityReportsApp from '../vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin'; import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin';
......
...@@ -10,22 +10,19 @@ export default class MRWidgetService extends CEWidgetService { ...@@ -10,22 +10,19 @@ export default class MRWidgetService extends CEWidgetService {
} }
fetchApprovals() { fetchApprovals() {
return axios.get(this.approvalsPath) return axios.get(this.approvalsPath).then(res => res.data);
.then(res => res.data);
} }
approveMergeRequest() { approveMergeRequest() {
return axios.post(this.approvalsPath) return axios.post(this.approvalsPath).then(res => res.data);
.then(res => res.data);
} }
unapproveMergeRequest() { unapproveMergeRequest() {
return axios.delete(this.approvalsPath) return axios.delete(this.approvalsPath).then(res => res.data);
.then(res => res.data);
} }
fetchReport(endpoint) { // eslint-disable-line // eslint-disable-next-line class-methods-use-this
return axios.get(endpoint) fetchReport(endpoint) {
.then(res => res.data); return axios.get(endpoint).then(res => res.data);
} }
} }
import CEGetStateKey from '~/vue_merge_request_widget/stores/get_state_key'; import CEGetStateKey from '~/vue_merge_request_widget/stores/get_state_key';
export default function (data) { export default function(data) {
if (this.isGeoSecondaryNode) { if (this.isGeoSecondaryNode) {
return 'geoSecondaryNode'; return 'geoSecondaryNode';
} }
return CEGetStateKey.call(this, data); return CEGetStateKey.call(this, data);
} }
<script>
// Analogue of link_to_member_avatar in app/helpers/projects_helper.rb // Analogue of link_to_member_avatar in app/helpers/projects_helper.rb
import pendingAvatarSvg from 'ee_icons/_icon_dotted_circle.svg'; import pendingAvatarSvg from 'ee_icons/_icon_dotted_circle.svg';
...@@ -6,6 +7,7 @@ export default { ...@@ -6,6 +7,7 @@ export default {
avatarUrl: { avatarUrl: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
profileUrl: { profileUrl: {
type: String, type: String,
...@@ -15,6 +17,7 @@ export default { ...@@ -15,6 +17,7 @@ export default {
displayName: { displayName: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
extraAvatarClass: { extraAvatarClass: {
type: String, type: String,
...@@ -39,10 +42,7 @@ export default { ...@@ -39,10 +42,7 @@ export default {
tooltipContainer: { tooltipContainer: {
type: String, type: String,
required: false, required: false,
}, default: 'body',
avatarHtml: {
type: String,
required: false,
}, },
avatarSize: { avatarSize: {
type: Number, type: Number,
...@@ -75,32 +75,33 @@ export default { ...@@ -75,32 +75,33 @@ export default {
linkClass() { linkClass() {
return `author_link ${this.tooltipClass} ${this.extraLinkClass} ${this.disabledClass}`; 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 @@ ...@@ -22,7 +22,6 @@
align-items: flex-start; align-items: flex-start;
.close { .close {
.dismiss-icon { .dismiss-icon {
color: $gray-darkest; color: $gray-darkest;
} }
...@@ -32,13 +31,11 @@ ...@@ -32,13 +31,11 @@
color: $text-color; color: $text-color;
} }
} }
} }
.svg-container { .svg-container {
margin-right: 15px; margin-right: 15px;
} }
} }
&.promotion-empty-page { &.promotion-empty-page {
...@@ -70,7 +67,6 @@ ...@@ -70,7 +67,6 @@
} }
.promotion-modal { .promotion-modal {
.modal-dialog { .modal-dialog {
width: 540px; width: 540px;
} }
...@@ -87,7 +83,6 @@ ...@@ -87,7 +83,6 @@
.modal-footer { .modal-footer {
border-top: 0; border-top: 0;
} }
} }
.promotion-backdrop { .promotion-backdrop {
...@@ -100,11 +95,17 @@ ...@@ -100,11 +95,17 @@
} }
} }
.promotion-info-weight-message { .promotion-issue-sidebar {
padding: $gl-padding-top; .promotion-issue-sidebar-message {
padding: $gl-padding-top;
.dropdown-title { .dropdown-title {
margin: 0 0 10px; margin: 0 0 10px;
}
.btn + .btn {
margin-top: $gl-padding-4;
}
} }
.btn { .btn {
...@@ -114,7 +115,7 @@ ...@@ -114,7 +115,7 @@
line-height: $line-height-base; line-height: $line-height-base;
} }
.btn-link { .right-sidebar & .btn-link {
display: inline; display: inline;
color: $gl-link-color; color: $gl-link-color;
background-color: transparent; background-color: transparent;
...@@ -127,7 +128,6 @@ ...@@ -127,7 +128,6 @@
} }
} }
.promotion-issue-template-message { .promotion-issue-template-message {
padding: $gl-padding 30px 20px $gl-padding; padding: $gl-padding 30px 20px $gl-padding;
...@@ -160,4 +160,3 @@ ...@@ -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 ...@@ -51,12 +51,10 @@ module EE
if succeeded if succeeded
all_projects.each do |project| all_projects.each do |project|
old_path_with_namespace = File.join(full_path_was, project.path)
::Geo::RepositoryRenamedEventStore.new( ::Geo::RepositoryRenamedEventStore.new(
project, project,
old_path: project.path, old_path: project.path,
old_path_with_namespace: old_path_with_namespace old_path_with_namespace: old_path_with_namespace_for(project)
).create ).create
end end
end end
...@@ -64,6 +62,10 @@ module EE ...@@ -64,6 +62,10 @@ module EE
succeeded succeeded
end 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 # Checks features (i.e. https://about.gitlab.com/products/) availabily
# for a given Namespace plan. This method should consider ancestor groups # for a given Namespace plan. This method should consider ancestor groups
# being licensed. # being licensed.
......
- return unless issuable.project.group&.feature_available?(:epics)
- if issuable.is_a?(Issue) - if issuable.is_a?(Issue)
.block.epic .block.epic
#js-vue-sidebar-item-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) - promotion_feature = 'promote_weight_sidebar_dismissed'
.block.weight
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".weight" } } - 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") } %span{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: _("Weight") }
= icon('balance-scale') = icon('balance-scale')
%span No %span No
.title.hide-collapsed .title.hide-collapsed
= _('Weight') = _('Weight')
= link_to _('Edit'), '#', class: 'edit-link promote-weight-link pull-right', data: { toggle: "dropdown", target: ".weight" } .dropdown
.promotion-info-weight.dropdown .dropdown-menu.promotion-issue-sidebar-message
.dropdown-menu.promotion-info-weight-message
.dropdown-title .dropdown-title
%span %span
= _('Change Weight') = _('Change Weight')
...@@ -23,6 +24,7 @@ ...@@ -23,6 +24,7 @@
= link_to _('Read more'), help_page_path('workflow/issue_weight.html'), class: 'btn-link', target: '_blank' = link_to _('Read more'), help_page_path('workflow/issue_weight.html'), class: 'btn-link', target: '_blank'
%div %div
= render 'shared/promotions/promotion_link_project', short_form: true = 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 .hide-collapsed
%span.no-value = s_('Promotions|This feature is locked.')
= _('None') = 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 ...@@ -16,7 +16,6 @@ module Gitlab
delete delete
delete_all delete_all
insert insert
transaction
update update
update_all update_all
).freeze ).freeze
...@@ -46,6 +45,19 @@ module Gitlab ...@@ -46,6 +45,19 @@ module Gitlab
end end
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. # Delegates all unknown messages to a read-write connection.
def method_missing(name, *args, &block) def method_missing(name, *args, &block)
write_using_load_balancer(name, args, &block) write_using_load_balancer(name, args, &block)
...@@ -55,9 +67,7 @@ module Gitlab ...@@ -55,9 +67,7 @@ module Gitlab
# #
# name - The name of the method to call on a connection object. # name - The name of the method to call on a connection object.
def read_using_load_balancer(name, args, &block) def read_using_load_balancer(name, args, &block)
method = Session.current.use_primary? ? :read_write : :read @load_balancer.send(load_balancer_method_for_read) do |connection|
@load_balancer.send(method) do |connection|
connection.send(name, *args, &block) connection.send(name, *args, &block)
end end
end end
...@@ -77,8 +87,45 @@ module Gitlab ...@@ -77,8 +87,45 @@ module Gitlab
connection.send(name, *args, &block) connection.send(name, *args, &block)
end 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 result
end 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 end
end end
......
...@@ -9,6 +9,8 @@ module Gitlab ...@@ -9,6 +9,8 @@ module Gitlab
class Session class Session
CACHE_KEY = :gitlab_load_balancer_session CACHE_KEY = :gitlab_load_balancer_session
attr_accessor :last_write_location
def self.current def self.current
RequestStore[CACHE_KEY] ||= new RequestStore[CACHE_KEY] ||= new
end end
...@@ -18,6 +20,13 @@ module Gitlab ...@@ -18,6 +20,13 @@ module Gitlab
end end
def initialize def initialize
@transaction_nesting = 0
reset!
end
def reset!
@last_write_location = nil
@use_primary = false @use_primary = false
@performed_write = false @performed_write = false
end end
...@@ -26,6 +35,18 @@ module Gitlab ...@@ -26,6 +35,18 @@ module Gitlab
@use_primary @use_primary
end 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! def use_primary!
@use_primary = true @use_primary = true
end end
......
...@@ -18,6 +18,7 @@ describe 'Promotions', :js do ...@@ -18,6 +18,7 @@ describe 'Promotions', :js do
it 'should show no promotion at all' do it 'should show no promotion at all' do
sign_in(user) sign_in(user)
visit edit_project_path(project) visit edit_project_path(project)
expect(page).not_to have_selector('#promote_service_desk') expect(page).not_to have_selector('#promote_service_desk')
end end
end end
...@@ -33,12 +34,14 @@ describe 'Promotions', :js do ...@@ -33,12 +34,14 @@ describe 'Promotions', :js do
it 'should have the contact admin line' do it 'should have the contact admin line' do
sign_in(user) sign_in(user)
visit edit_project_path(project) visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Contact your Administrator to upgrade your license.' expect(find('#promote_service_desk')).to have_content 'Contact your Administrator to upgrade your license.'
end end
it 'should have the start trial button' do it 'should have the start trial button' do
sign_in(admin) sign_in(admin)
visit edit_project_path(project) visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Start GitLab Ultimate trial' expect(find('#promote_service_desk')).to have_content 'Start GitLab Ultimate trial'
end end
end end
...@@ -58,11 +61,13 @@ describe 'Promotions', :js do ...@@ -58,11 +61,13 @@ describe 'Promotions', :js do
it 'should have the Upgrade your plan button' do it 'should have the Upgrade your plan button' do
visit edit_project_path(project) visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Upgrade your plan' expect(find('#promote_service_desk')).to have_content 'Upgrade your plan'
end end
it 'should have the contact owner line' do it 'should have the contact owner line' do
visit edit_project_path(otherproject) visit edit_project_path(otherproject)
expect(find('#promote_service_desk')).to have_content 'Contact owner' expect(find('#promote_service_desk')).to have_content 'Contact owner'
end end
end end
...@@ -79,6 +84,7 @@ describe 'Promotions', :js do ...@@ -79,6 +84,7 @@ describe 'Promotions', :js do
it 'should appear in project edit page' do it 'should appear in project edit page' do
visit edit_project_path(project) visit edit_project_path(project)
expect(find('#promote_service_desk')).to have_content 'Improve customer support with GitLab Service Desk.' expect(find('#promote_service_desk')).to have_content 'Improve customer support with GitLab Service Desk.'
end end
...@@ -108,6 +114,7 @@ describe 'Promotions', :js do ...@@ -108,6 +114,7 @@ describe 'Promotions', :js do
it 'should appear in project edit page' do it 'should appear in project edit page' do
visit edit_project_path(project) visit edit_project_path(project)
expect(find('#promote_mr_features')).to have_content 'Improve Merge Requests' expect(find('#promote_mr_features')).to have_content 'Improve Merge Requests'
end end
...@@ -165,6 +172,7 @@ describe 'Promotions', :js do ...@@ -165,6 +172,7 @@ describe 'Promotions', :js do
it 'should appear in new MR page' do it 'should appear in new MR page' do
visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'feature' }) 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.' expect(find('#promote_squash_commits')).to have_content 'Improve Merge Requests with Squash Commit and GitLab Enterprise Edition.'
end end
...@@ -192,6 +200,7 @@ describe 'Promotions', :js do ...@@ -192,6 +200,7 @@ describe 'Promotions', :js do
it 'should appear in milestone page' do it 'should appear in milestone page' do
visit project_milestone_path(project, milestone) visit project_milestone_path(project, milestone)
expect(find('#promote_burndown_charts')).to have_content "Upgrade your plan to improve milestones with Burndown Charts." expect(find('#promote_burndown_charts')).to have_content "Upgrade your plan to improve milestones with Burndown Charts."
end end
...@@ -219,6 +228,7 @@ describe 'Promotions', :js do ...@@ -219,6 +228,7 @@ describe 'Promotions', :js do
it 'should appear in milestone page' do it 'should appear in milestone page' do
visit project_boards_path(project) visit project_boards_path(project)
expect(find('.board-promotion-state')).to have_content "Upgrade your plan to improve Issue boards" expect(find('.board-promotion-state')).to have_content "Upgrade your plan to improve Issue boards"
end end
...@@ -246,11 +256,64 @@ describe 'Promotions', :js do ...@@ -246,11 +256,64 @@ describe 'Promotions', :js do
it 'should appear on export modal' do it 'should appear on export modal' do
visit project_issues_path(project) visit project_issues_path(project)
click_on 'Export as CSV' click_on 'Export as CSV'
expect(find('.issues-export-modal')).to have_content 'Export issues with GitLab Enterprise Edition.' expect(find('.issues-export-modal')).to have_content 'Export issues with GitLab Enterprise Edition.'
end end
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 describe 'for issue weight', :js do
before do before do
allow(License).to receive(:current).and_return(nil) allow(License).to receive(:current).and_return(nil)
...@@ -263,8 +326,42 @@ describe 'Promotions', :js do ...@@ -263,8 +326,42 @@ describe 'Promotions', :js do
it 'should appear on the page', :js do it 'should appear on the page', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests 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
end end
...@@ -280,7 +377,9 @@ describe 'Promotions', :js do ...@@ -280,7 +377,9 @@ describe 'Promotions', :js do
it 'should appear on the page', :js do it 'should appear on the page', :js do
visit new_project_issue_path(project) visit new_project_issue_path(project)
wait_for_requests wait_for_requests
find('#promotion-issue-template-link').click 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.' 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
end end
...@@ -296,6 +395,7 @@ describe 'Promotions', :js do ...@@ -296,6 +395,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do it 'should appear on the page' do
visit project_audit_events_path(project) visit project_audit_events_path(project)
expect(find('.user-callout-copy')).to have_content 'Track your project with Audit Events' expect(find('.user-callout-copy')).to have_content 'Track your project with Audit Events'
end end
end end
...@@ -311,6 +411,7 @@ describe 'Promotions', :js do ...@@ -311,6 +411,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do it 'should appear on the page' do
visit group_analytics_path(group) visit group_analytics_path(group)
expect(find('.user-callout-copy')).to have_content 'Track activity with Contribution Analytics.' expect(find('.user-callout-copy')).to have_content 'Track activity with Contribution Analytics.'
end end
end end
...@@ -326,6 +427,7 @@ describe 'Promotions', :js do ...@@ -326,6 +427,7 @@ describe 'Promotions', :js do
it 'should appear on the page' do it 'should appear on the page' do
visit group_hooks_path(group) visit group_hooks_path(group)
expect(find('.user-callout-copy')).to have_content 'Add Group Webhooks' expect(find('.user-callout-copy')).to have_content 'Add Group Webhooks'
end end
end end
......
...@@ -49,8 +49,16 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do ...@@ -49,8 +49,16 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
# We have an extra test for #transaction here to make sure that nested queries # We have an extra test for #transaction here to make sure that nested queries
# are also sent to a primary. # are also sent to a primary.
describe '#transaction' do describe '#transaction' do
after do let(:session) { Gitlab::Database::LoadBalancing::Session.new }
Gitlab::Database::LoadBalancing::Session.clear_session
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 end
it 'runs the transaction and any nested queries on the primary' do it 'runs the transaction and any nested queries on the primary' do
...@@ -60,15 +68,36 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do ...@@ -60,15 +68,36 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
allow(primary).to receive(:select) allow(primary).to receive(:select)
expect(proxy.load_balancer).to receive(:read_write) 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. # This expectation is put in place to ensure no read is performed.
expect(proxy.load_balancer).not_to receive(:read) expect(proxy.load_balancer).not_to receive(:read)
proxy.transaction { proxy.select('true') } proxy.transaction { proxy.select('true') }
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?) expect(session.use_primary?).to eq(true)
.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
end end
...@@ -91,62 +120,136 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do ...@@ -91,62 +120,136 @@ describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end end
describe '#read_using_load_balancer' do describe '#read_using_load_balancer' do
let(:session) { double(:session) } let(:session) { Gitlab::Database::LoadBalancing::Session.new }
let(:connection) { double(:connection) } let(:connection) { double(:connection) }
before do before do
allow(Gitlab::Database::LoadBalancing::Session).to receive(:current) allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session) .and_return(session)
end end
describe 'with a regular session' do it 'performs a read-only query' do
it 'uses a secondary' do allow(proxy.load_balancer)
allow(session).to receive(:use_primary?).and_return(false) .to receive(:load_balancer_method_for_read)
.and_return(:read)
expect(connection).to receive(:foo).with('foo') allow(proxy.load_balancer)
expect(proxy.load_balancer).to receive(:read).and_yield(connection) .to receive(:read)
.and_yield(connection)
proxy.read_using_load_balancer(:foo, ['foo'])
end
end
describe 'with a session using the primary' do expect(connection)
it 'uses the primary' do .to receive(:foo)
allow(session).to receive(:use_primary?).and_return(true) .with('foo')
expect(connection).to receive(:foo).with('foo') proxy.read_using_load_balancer(:foo, ['foo'])
expect(proxy.load_balancer).to receive(:read_write)
.and_yield(connection)
proxy.read_using_load_balancer(:foo, ['foo'])
end
end end
end end
describe '#write_using_load_balancer' do describe '#write_using_load_balancer' do
let(:session) { double(:session) } let(:session) { Gitlab::Database::LoadBalancing::Session.new }
let(:connection) { double(:connection) } let(:connection) { double(:connection) }
before do before do
allow(Gitlab::Database::LoadBalancing::Session).to receive(:current) allow(Gitlab::Database::LoadBalancing::Session)
.to receive(:current)
.and_return(session) .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 end
it 'it uses but does not stick to the primary when sticking is disabled' do 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!) expect(session).not_to receive(:write!)
proxy.write_using_load_balancer(:foo, ['foo']) proxy.write_using_load_balancer(:foo, ['foo'])
end end
it 'sticks to the primary when sticking is enabled' do 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!) expect(session).to receive(:write!)
proxy.write_using_load_balancer(:foo, ['foo'], sticky: true) proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
end 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
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