Commit fc550b39 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/multi-level-container-registry-images

* master: (230 commits)
  Fix N+1 query in loading pipelines in merge requests
  Fix Spinach and Capybara dependencies
  Prevent users from disconnecting gitlab account from CAS
  30276 Move issue, mr, todos next to profile dropdown in top nav
  Refactor SearchController#show
  Properly eagerly-load the Capybara server for JS feature specs only
  Updating documentation to include a missing step in the update procedure
  Eager-load the Capybara server to prevent timeouts
  Increase Capybara's timeout
  Add metrics button to Environment Overview page
  Fix link to Jira service documentation
  Handle parsing OpenBSD ps output properly to display sidekiq infos on ...
  Eliminate unnecessary queries that add ~500 ms of load time for a large issue
  20914 Limits line length for project home page
  Allow users to import GitHub projects to subgroups
  Update dpl CI example
  Fix the docs:check:links job
  Don't clean up the gitlab-test-fork_bare repo
  Make GitLab use Gitaly for commit_is_ancestor
  Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery
  ...
parents 83d1fe9b e7e93072
......@@ -14,13 +14,15 @@ variables:
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS'
- retry gem install knapsack
- retry gem install knapsack fog-aws mime-types
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
stages:
......@@ -39,14 +41,15 @@ stages:
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache:
key: "knapsack"
paths:
- knapsack/
- knapsack/
artifacts:
expire_in: 31d
paths:
- knapsack/
- knapsack/
.use-db: &use-db
services:
......@@ -61,17 +64,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
when: always
paths:
- coverage/
- knapsack/
- tmp/capybara/
- coverage/
- knapsack/
- tmp/capybara/
.spinach-knapsack: &spinach-knapsack
stage: test
......@@ -81,28 +84,44 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
paths:
- coverage/
- knapsack/
- tmp/capybara/
- coverage/
- knapsack/
- tmp/capybara/
# Prepare and merge knapsack tests
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: prepare
script:
- mkdir -p knapsack/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
- mkdir -p knapsack/${CI_PROJECT_NAME}/
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
setup-test-env:
<<: *use-db
......@@ -122,20 +141,6 @@ setup-test-env:
- public/assets
- tmp/tests
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
......@@ -287,14 +292,35 @@ rake karma:
paths:
- coverage-javascript/
lint-doc:
docs:check:apilint:
image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
image: "phusion/baseimage:latest"
variables:
GIT_DEPTH: "3"
cache: {}
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
variables:
GIT_DEPTH: "3"
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:check:
stage: test
<<: *dedicated-runner
......
......@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.0.2 (2017-03-29)
- Correctly update paths when changing a child group.
- Fixed private group name disclosure via new/update forms.
## 9.0.1 (2017-03-28)
- Resolve "404 when requesting build trace". !9759 (dosuken123)
- Simplify search queries for projects and merge requests. !10053 (mhasbini)
- Fix after_script processing for Runners APIv4. !10185
- Fix escaped html appearing in milestone page. !10224
- Fix bug that caused jobs that already had been retried to be retried again when retrying a pipeline. !10249
- Allow filtering by all started milestones.
- Allow sorting by due date and priority.
- Fixed branches pagination not displaying.
- Fixed filtered search not working in IE.
- Optimize labels finder query when searching for a project with a group. (mhasbini)
## 9.0.0 (2017-03-22)
- Fix inconsistent naming for services that delete things. !5803 (dixpac)
......
......@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch
1. Leave the approvals settings as they are:
1. Your merge request needs at least 1 approval
1. You don't have to select any approvers
1. Your merge request needs at least 1 approval but feel free to require more.
For instance if you're touching backend and frontend code, it's a good idea
to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
maintainer
1. You don't have to select any approvers, but you can if you really want
specific people to approve your merge request
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
used to achieve it.
......@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][team]**.
1. If your merge request includes frontend and backend changes [^1], it must
be approved by a frontend **and** a backend maintainer.
be **approved by a [frontend and a backend maintainer][team]**.
1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][team] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free
......@@ -556,6 +559,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml
changes are considered backend code if they include Ruby code other than just
pure HTML.
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
......@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
gem 'rugged', '~> 0.25.1.1'
# Authentication libraries
gem 'devise', '~> 4.2'
......@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
......
......@@ -288,9 +288,9 @@ GEM
rouge (~> 2.0)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3)
rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
......@@ -682,7 +682,7 @@ GEM
rubypants (0.2.0)
rubyzip (1.2.1)
rufus-scheduler (3.1.10)
rugged (0.24.0)
rugged (0.25.1.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -905,7 +905,7 @@ DEPENDENCIES
gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.19.0)
......@@ -987,7 +987,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
......
8.18.0-pre
9.1.0-pre
import spreadString from './spread_string';
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
......@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
......@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
......@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => {
Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
......
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
function knownCharCodeAt(givenString, index) {
const str = `${givenString}`;
const end = str.length;
const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
let idx = index;
while ((surrogatePairs.exec(str)) != null) {
const li = surrogatePairs.lastIndex;
if (li - 2 < idx) {
idx += 1;
} else {
break;
}
}
if (idx >= end || idx < 0) {
return NaN;
}
const code = str.charCodeAt(idx);
let high;
let low;
if (code >= 0xD800 && code <= 0xDBFF) {
high = code;
low = str.charCodeAt(idx + 1);
// Go one further, since one of the "characters" is part of a surrogate pair
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
return code;
}
// See http://stackoverflow.com/a/38901550/796832
// ES5/PhantomJS compatible version of spreading a string
//
// [...'foo'] -> ['f', 'o', 'o']
// [...'🖐🏿'] -> ['🖐', '🏿']
function spreadString(str) {
const arr = [];
let i = 0;
while (!isNaN(knownCharCodeAt(str, i))) {
const codePoint = knownCharCodeAt(str, i);
arr.push(String.fromCodePoint(codePoint));
i += 1;
}
return arr;
}
export default spreadString;
......@@ -18,7 +18,7 @@
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
// %a.js-toggle-button
// %button.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
......
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab';
Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="iPython notebook loading">
</i>
</div>
<notebook-lab
v-if="!loading && !error"
:notebook="json"
code-css-class="code white" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst parsing the file.
</span>
</p>
</div>
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then((res) => {
this.json = res.json();
this.loading = false;
})
.catch((e) => {
if (e.status) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
});
};
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
......@@ -79,7 +79,7 @@ $(() => {
resp.json().forEach((board) => {
const list = Store.addList(board);
if (list.type === 'done') {
if (list.type === 'closed') {
list.position = Infinity;
}
});
......
......@@ -50,9 +50,7 @@ export default {
this.showDetail = false;
},
showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
......
......@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }}
</span>
<a
class="card-assignee has-tooltip"
class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
class="avatar avatar-inline s20"
class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
class="label color-label has-tooltip"
class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
......
......@@ -48,7 +48,7 @@ import Vue from 'vue';
template: `
<div
class="block list"
v-if="list.type !== 'done'">
v-if="list.type !== 'closed'">
<button
class="btn btn-default btn-block"
type="button"
......
......@@ -10,7 +10,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1;
this.loading = true;
this.loadingMore = false;
......
......@@ -45,7 +45,7 @@ import Cookies from 'js-cookie';
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'done')[0]);
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -98,7 +98,7 @@ import Cookies from 'js-cookie';
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'done') {
if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
......@@ -33,12 +33,11 @@ export default Vue.component('pipelines-table', {
* @return {Object}
*/
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const store = new PipelineStore();
return {
endpoint: pipelinesTableData.endpoint,
helpPagePath: pipelinesTableData.helpPagePath,
endpoint: null,
helpPagePath: null,
store,
state: store.state,
isLoading: false,
......@@ -65,6 +64,8 @@ export default Vue.component('pipelines-table', {
*
*/
beforeMount() {
this.endpoint = this.$el.dataset.endpoint;
this.helpPagePath = this.$el.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint);
this.fetchPipelines();
......
......@@ -33,6 +33,8 @@
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
/* global Sidebar */
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
......@@ -118,6 +120,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
new Sidebar();
break;
case 'dashboard:todos:index':
new gl.Todos();
......
......@@ -25,6 +25,12 @@ export default {
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
......@@ -44,8 +50,11 @@ export default {
template: `
<div class="btn-group" role="group">
<button
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
:title="title"
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
......
......@@ -9,13 +9,21 @@ export default {
},
},
computed: {
title() {
return 'Open';
},
},
template: `
<a
class="btn external_url"
class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl"
target="_blank"
rel="noopener noreferrer"
title="Environment external URL">
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
......
......@@ -5,6 +5,7 @@ import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit';
/**
......@@ -22,6 +23,7 @@ export default {
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
},
props: {
......@@ -392,6 +394,14 @@ export default {
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
/**
* Constructs folder URL based on the current location and the folder id.
*
......@@ -496,13 +506,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
......
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
export default {
props: {
monitoringUrl: {
type: String,
default: '',
required: true,
},
},
computed: {
title() {
return 'Monitoring';
},
},
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
};
......@@ -25,6 +25,12 @@ export default {
};
},
computed: {
title() {
return 'Stop';
},
},
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
......@@ -45,10 +51,12 @@ export default {
template: `
<button type="button"
class="btn stop-env-link"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
title="Stop Environment">
:title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
......
......@@ -14,12 +14,22 @@ export default {
},
data() {
return { terminalIconSvg };
return {
terminalIconSvg,
};
},
computed: {
title() {
return 'Terminal';
},
},
template: `
<a class="btn terminal-button"
title="Open web terminal"
<a class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
......
......@@ -8,21 +8,31 @@ require('./filtered_search_token_keys');
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
const tokenIndexes = []; // stores key+value for simple search
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
......
const GROUP_LIMIT = 2;
import _ from 'underscore';
export default class GroupName {
constructor() {
this.titleContainer = document.querySelector('.title');
this.groups = document.querySelectorAll('.group-path');
this.titleContainer = document.querySelector('.title-container');
this.title = document.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
if (this.groups.length > GROUP_LIMIT) {
if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
this.addToggle();
this.toggleHandler();
window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
addToggle() {
const header = document.querySelector('.header-content');
toggleHandler() {
if (this.titleWidth > this.titleContainer.offsetWidth) {
if (!this.toggle) this.createToggle();
this.showToggle();
} else if (this.toggle) {
this.hideToggle();
}
}
createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
header.insertBefore(this.toggle, this.titleContainer);
this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
showToggle() {
this.title.classList.add('wrap');
this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('is-hidden');
}
hideToggle() {
this.title.classList.remove('wrap');
this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
}
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
this.titleContainer.classList.remove('initializing');
this.title.classList.remove('initializing');
}
}
......@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) {
var all_available, skip_groups;
all_available = $(select).data('all-available');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
var options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
results: groups
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
skip_groups,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const results = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
return {
results,
page,
more,
};
return query.callback(data);
});
},
},
initSelection: function(element, callback) {
var id;
......@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args);
return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatSelection.apply(_this, args);
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown",
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
......@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name;
};
GroupsSelect.prototype.forceOverflow = function (e) {
const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
};
GroupsSelect.PER_PAGE = 20;
return GroupsSelect;
})();
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count');
var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
......@@ -11,8 +11,9 @@
});
};
$(function() {
var $scrollingTabs = $('.scrolling-tabs');
$(document).on('init.scrolling-tabs', () => {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
......
......@@ -231,6 +231,22 @@
return upperCaseHeaders;
};
/**
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeCRLFHeaders = (headers) => {
const headersObject = {};
const headersArray = headers.split('\n');
headersArray.forEach((header) => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
return w.gl.utils.normalizeHeaders(headersObject);
};
/**
* Parses pagination object string values into numbers.
*
......
......@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status';
* Service for vue resouce and method need to be provided as props
*
* @example
* new poll({
* new Poll({
* resource: resource,
* method: 'name',
* data: {page: 1, scope: 'all'},
* data: {page: 1, scope: 'all'}, // optional
* successCallback: () => {},
* errorCallback: () => {},
* notificationCallback: () => {}, // optional
* }).makeRequest();
*
* this.service = new BoardsService(endpoint);
* new poll({
* resource: this.service,
* method: 'get',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
* Usage in pipelines table with visibility lib:
*
* const poll = new Poll({
* resource: this.service,
* method: 'getPipelines',
* data: { page: pageNumber, scope },
* successCallback: this.successCallback,
* errorCallback: this.errorCallback,
* notificationCallback: this.updateLoading,
* });
*
* if (!Visibility.hidden()) {
* poll.makeRequest();
* }
*
* Visibility.change(() => {
* if (!Visibility.hidden()) {
* poll.restart();
* } else {
* poll.stop();
* }
* });
*
* 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header.
......@@ -34,6 +48,8 @@ export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback ||
function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
......@@ -42,7 +58,7 @@ export default class Poll {
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader];
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
......@@ -54,11 +70,14 @@ export default class Poll {
}
makeRequest() {
const { resource, method, data, errorCallback } = this.options;
const { resource, method, data, errorCallback, notificationCallback } = this.options;
// It's called everytime a new request is made. Useful to update the status.
notificationCallback(true);
return resource[method](data)
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
/**
......@@ -70,4 +89,12 @@ export default class Poll {
this.canPoll = false;
clearTimeout(this.timeoutID);
}
/**
* Restarts polling after it has been stoped
*/
restart() {
this.canPoll = true;
this.makeRequest();
}
}
......@@ -370,4 +370,6 @@ $(function () {
new Aside();
gl.utils.initTimeagoTimeout();
$(document).trigger('init.scrolling-tabs');
});
......@@ -4,8 +4,10 @@
import Cookies from 'js-cookie';
require('./breakpoints');
require('./flash');
import CommitPipelinesTable from './commit/pipelines/pipelines_table';
import './breakpoints';
import './flash';
/* eslint-disable max-len */
// MergeRequestTabs
......@@ -97,6 +99,13 @@ require('./flash');
.off('click', this.clickTab);
}
destroy() {
this.unbindEvents();
if (this.commitPipelinesTable) {
this.commitPipelinesTable.$destroy();
}
}
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
......@@ -128,12 +137,8 @@ require('./flash');
this.expandViewContainer();
}
} else if (action === 'pipelines') {
if (this.pipelinesLoaded) {
return;
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
this.pipelinesLoaded = true;
this.resetViewContainer();
this.loadPipelines();
} else {
this.expandView();
this.resetViewContainer();
......@@ -222,6 +227,18 @@ require('./flash');
});
}
loadPipelines() {
if (this.pipelinesLoaded) {
return;
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
// Could already be mounted from the `pipelines_bundle`
if (pipelineTableViewEl) {
this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
}
this.pipelinesLoaded = true;
}
loadDiff(source) {
if (this.diffsLoaded) {
return;
......
......@@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
......
......@@ -6,7 +6,7 @@ class ProtectedBranchDropdown {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
......@@ -46,7 +46,9 @@ class ProtectedBranchDropdown {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard() {
onClickCreateWildcard(e) {
e.preventDefault();
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
......@@ -69,7 +71,7 @@ class ProtectedBranchDropdown {
if (branchName) {
this.$dropdownContainer
.find('.create-new-protected-branch code')
.find('.js-create-new-protected-branch code')
.text(branchName);
}
......
......@@ -56,14 +56,15 @@ import Cookies from 'js-cookie';
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
$todoLoading = $('.js-issuable-todo-loading');
$btnText = $('.js-issuable-todo-text', $this);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
url = "" + ($this.data('url'));
}
$this.tooltip('hide');
return $.ajax({
url: url,
type: ajaxType,
......@@ -74,34 +75,44 @@ import Cookies from 'js-cookie';
},
beforeSend: (function(_this) {
return function() {
return _this.beforeTodoSend($this, $todoLoading);
$('.js-issuable-todo').disable()
.addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
return _this.todoUpdateDone(data, $this, $btnText, $todoLoading);
return _this.todoUpdateDone(data);
};
})(this));
};
Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) {
$btn.disable();
return $todoLoading.removeClass('hidden');
};
Sidebar.prototype.todoUpdateDone = function(data) {
const deletePath = data.delete_path ? data.delete_path : null;
const attrPrefix = deletePath ? 'mark' : 'todo';
const $todoBtns = $('.js-issuable-todo');
Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
$(document).trigger('todo:toggle', data.count);
$btn.enable();
$todoLoading.addClass('hidden');
$todoBtns.each((i, el) => {
const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner');
if (data.delete_path != null) {
$btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
return $btnText.text($btn.data('mark-text'));
} else {
$btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path');
return $btnText.text($btn.data('todo-text'));
}
$el.removeClass('is-loading')
.enable()
.attr('aria-label', $el.data(`${attrPrefix}-text`))
.attr('data-delete-path', deletePath)
.attr('title', $el.data(`${attrPrefix}-text`));
if ($el.hasClass('has-tooltip')) {
$el.tooltip('fixTitle');
}
if ($el.data(`${attrPrefix}-icon`)) {
$elText.html($el.data(`${attrPrefix}-icon`));
} else {
$elText.text($el.data(`${attrPrefix}-text`));
}
});
};
Sidebar.prototype.sidebarDropdownLoading = function(e) {
......@@ -198,7 +209,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
......
import Cookies from 'js-cookie';
const userCalloutElementName = '.user-callout';
const closeButton = '.close-user-callout';
const userCalloutBtn = '.user-callout-btn';
const userCalloutSvgAttrName = 'callout-svg';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const USER_CALLOUT_TEMPLATE = `
<div class="bordered-box landing content-block">
<button class="btn btn-default close close-user-callout" type="button">
<i class="fa fa-times dismiss-icon"></i>
</button>
<div class="row">
<div class="col-sm-3 col-xs-12 svg-container">
</div>
<div class="col-sm-8 col-xs-12 inner-content">
<h4>
Customize your experience
</h4>
<p>
Change syntax themes, default project pages, and more in preferences.
</p>
<a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
</div>
</div>
</div>`;
export default class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName);
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.userCalloutBody = $('.user-callout');
this.init();
}
init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg);
this.userCalloutBody.append($template);
$template.find(closeButton).on('click', e => this.dismissCallout(e));
$template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
} else {
this.userCalloutBody.remove();
$('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
}
}
......
......@@ -83,6 +83,7 @@ export default {
:class="buttonClass"
:title="title"
:aria-label="title"
data-container="body"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
......
......@@ -16,8 +16,12 @@ export default {
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
template: `
<ul class="nav-links">
<ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
......
......@@ -21,6 +21,7 @@ export default {
<li v-for="artifact in artifacts">
<a
rel="nofollow"
download
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
......
......@@ -182,8 +182,14 @@ export default {
<div :class="cssClass">
<div
class="top-area"
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
<div class="fade-left">
<i class="fa fa-angle-left" aria-hidden="true"></i>
</div>
<div class="fade-right">
<i class="fa fa-angle-right" aria-hidden="true"></i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
......
......@@ -3,8 +3,8 @@ const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
const FIRST = '« First';
const LAST = 'Last »';
export default {
props: {
......
......@@ -362,3 +362,13 @@
width: 100%;
}
}
.btn-blank {
padding: 0;
background: transparent;
border: 0;
&:focus {
outline: 0;
}
}
......@@ -119,6 +119,46 @@
}
}
@mixin dropdown-link {
display: block;
position: relative;
padding: 5px 8px;
color: $gl-text-color;
line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
&:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
.badge {
background-color: darken($dropdown-link-hover-bg, 5%);
}
}
&.dropdown-menu-empty-link {
&.is-focused {
background-color: $dropdown-empty-row-bg;
}
}
&.dropdown-menu-user-link {
line-height: 16px;
}
.icon-play {
fill: $gl-text-color-secondary;
margin-right: 6px;
height: 12px;
width: 11px;
}
}
.dropdown-menu,
.dropdown-menu-nav {
display: none;
......@@ -178,43 +218,7 @@
}
a {
display: block;
position: relative;
padding: 5px 8px;
color: $gl-text-color;
line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
&:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
.badge {
background-color: darken($dropdown-link-hover-bg, 5%);
}
}
&.dropdown-menu-empty-link {
&.is-focused {
background-color: $dropdown-empty-row-bg;
}
}
&.dropdown-menu-user-link {
line-height: 16px;
}
.icon-play {
fill: $gl-text-color-secondary;
margin-right: 6px;
height: 12px;
width: 11px;
}
@include dropdown-link;
}
.dropdown-header {
......
......@@ -26,7 +26,7 @@ header {
padding: 0 16px;
z-index: 100;
margin-bottom: 0;
height: $header-height;
min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
......@@ -48,10 +48,10 @@ header {
color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
margin: (($header-height - 28) / 2) 3px;
margin-left: 8px;
height: 28px;
min-width: 28px;
min-width: 32px;
line-height: 28px;
text-align: center;
......@@ -73,21 +73,29 @@ header {
background-color: $gray-light;
color: $gl-text-color;
.todos-pending-count {
background: darken($todo-alert-blue, 10%);
svg {
fill: $gl-text-color;
}
}
.fa-caret-down {
font-size: 14px;
}
svg {
position: relative;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
width: 23px;
fill: $gl-text-color-secondary;
}
}
.navbar-toggle {
color: $nav-toggle-gray;
margin: 6px 0;
margin: 5px 0;
border-radius: 0;
position: absolute;
right: -10px;
padding: 6px 10px;
......@@ -135,14 +143,12 @@ header {
}
.header-content {
display: flex;
justify-content: space-between;
position: relative;
height: $header-height;
min-height: $header-height;
padding-left: 30px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
.dropdown-menu {
margin-top: -5px;
}
......@@ -165,8 +171,7 @@ header {
}
.group-name-toggle {
margin: 0 5px;
vertical-align: sub;
margin: 3px 5px;
}
.group-title {
......@@ -177,39 +182,32 @@ header {
}
}
.title-container {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
padding-top: (($header-height - 19) / 2);
overflow: hidden;
}
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
max-width: 385px;
line-height: 22px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
&.initializing {
display: none;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) {
max-width: 190px;
&.wrap {
white-space: normal;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
max-width: 428px;
}
@media (min-width: $screen-lg-min) {
max-width: 685px;
&.initializing {
opacity: 0;
}
a {
......@@ -226,10 +224,10 @@ header {
border: transparent;
background: transparent;
position: absolute;
top: 2px;
right: 3px;
width: 12px;
line-height: 19px;
margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
......@@ -247,15 +245,12 @@ header {
}
.navbar-collapse {
float: right;
flex: 0 0 auto;
border-top: none;
@media (min-width: $screen-md-min) {
padding: 0;
}
padding: 0;
@media (max-width: $screen-xs-max) {
float: none;
flex: 1 1 auto;
}
}
}
......@@ -269,10 +264,30 @@ header {
}
}
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.header-content .title {
width: 300px;
.navbar-nav {
li {
.badge {
position: inherit;
top: -3px;
font-weight: normal;
margin-left: -12px;
font-size: 11px;
color: $white-light;
padding: 1px 5px 2px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
&.issues-count {
background-color: $green-500;
}
&.merge-requests-count {
background-color: $orange-600;
}
&.todos-count {
background-color: $blue-500;
}
}
}
}
......
......@@ -52,6 +52,18 @@
}
}
@mixin basic-list-stats {
.stats {
float: right;
line-height: $list-text-height;
color: $gl-text-color;
span {
margin-right: 15px;
}
}
}
@mixin bulleted-list {
> ul {
list-style-type: disc;
......
......@@ -146,6 +146,10 @@
display: block;
}
&.scrolling-tabs {
float: left;
}
li a {
padding: 16px 15px 11px;
}
......@@ -476,3 +480,44 @@
}
}
}
.inner-page-scroll-tabs {
position: relative;
.nav-links {
padding-bottom: 1px;
}
.fade-right {
@include fade(left, $white-light);
right: 0;
text-align: right;
.fa {
right: 5px;
}
}
.fade-left {
@include fade(right, $white-light);
left: 0;
text-align: left;
.fa {
left: 5px;
}
}
.fade-right,
.fade-left {
top: 16px;
bottom: auto;
}
&.is-smaller {
.fade-right,
.fade-left {
top: 11px;
}
}
}
......@@ -33,7 +33,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) {
.content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
......@@ -55,7 +55,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
......
......@@ -240,8 +240,13 @@
font-size: (14px / $issue-boards-font-size) * 1em;
}
.card-assignee {
margin-right: 5px;
}
.avatar {
margin-left: 0;
margin-right: 0;
}
}
......@@ -296,7 +301,7 @@
}
}
.issue-boards-sidebar {
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar {
top: 0;
bottom: 0;
......
......@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
line-height: 1;
font-size: $gl-font-size;
line-height: $gl-font-size;
outline: none;
&:hover {
background-color: darken($gray-light, 10%);
......
......@@ -431,6 +431,21 @@
border-bottom: none;
}
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-link-color;
transition: color 0.1s linear;
&:hover,
&:focus {
outline: none;
text-decoration: underline;
color: $gl-link-hover-color;
}
}
// Mobile
@media (max-width: 480px) {
.diff-title {
......
......@@ -26,7 +26,7 @@
.table.ci-table {
.environments-actions {
min-width: 200px;
min-width: 300px;
}
.environments-commit,
......@@ -222,3 +222,12 @@
stroke: $black;
stroke-width: 1;
}
.environments-actions {
.external-url,
.monitoring-url,
.terminal-button,
.stop-env-link {
width: 38px;
}
}
......@@ -9,16 +9,15 @@
}
}
.group-row {
.stats {
float: right;
line-height: $list-text-height;
color: $gl-text-color;
.group-root-path {
max-width: 40vw;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: nowrap;
}
span {
margin-right: 15px;
}
}
.group-row {
@include basic-list-stats;
}
.ldap-group-links {
......
......@@ -243,6 +243,10 @@
font-size: 13px;
font-weight: normal;
}
.hide-expanded {
display: none;
}
}
&.right-sidebar-collapsed {
......@@ -282,10 +286,11 @@
display: block;
width: 100%;
text-align: center;
padding-bottom: 10px;
margin-bottom: 10px;
color: $issuable-sidebar-color;
&:hover {
&:hover,
&:hover .todo-undone {
color: $gl-text-color;
}
......@@ -294,6 +299,10 @@
margin-top: 0;
}
.todo-undone {
color: $gl-link-color;
}
.author {
display: none;
}
......@@ -582,3 +591,21 @@
opacity: 0;
}
}
.issuable-todo-btn {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-spinner {
display: inline-block;
}
&.sidebar-collapsed-icon {
.issuable-todo-inner {
display: none;
}
}
}
}
......@@ -60,7 +60,17 @@
}
.modify-merge-commit-link {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-text-color;
&:hover,
&:focus {
text-decoration: underline;
}
}
.merge-param-checkbox {
......
......@@ -52,66 +52,62 @@
}
}
.milestone-summary {
.milestone-stat {
white-space: nowrap;
margin-right: 10px;
.milestone-sidebar {
.gutter-toggle {
margin-bottom: 10px;
}
&.with-drilldown {
margin-right: 2px;
.milestone-progress {
.title {
padding-top: 5px;
}
}
.remaining-days {
color: $orange-600;
.progress {
height: 6px;
margin: 0;
}
}
.milestone-stats-and-buttons {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
.collapsed-milestone-date {
font-size: 12px;
}
@media (min-width: $screen-xs-min) {
justify-content: space-between;
flex-wrap: nowrap;
}
.milestone-date {
display: block;
}
.milestone-progress-buttons {
order: 1;
margin-top: 10px;
.date-separator {
line-height: 5px;
}
@media (min-width: $screen-xs-min) {
order: 2;
margin-top: 0;
flex-shrink: 0;
}
.remaining-days strong {
font-weight: normal;
}
.btn {
float: left;
margin-right: $btn-side-margin;
.milestone-stat {
float: left;
margin-right: 14px;
}
&:last-child {
margin-right: 0;
}
}
.milestone-stat:last-child {
margin-right: 0;
}
.milestone-stats {
order: 2;
width: 100%;
padding: 7px 0;
flex-shrink: 1;
.milestone-progress {
.sidebar-collapsed-icon {
clear: both;
padding: 15px 5px 5px;
@media (min-width: $screen-xs-min) {
// when displayed on one line stats go first, buttons second
order: 1;
.progress {
margin: 5px 0;
}
}
}
.progress {
width: 100%;
margin: 15px 0;
.right-sidebar-collapsed & {
.reference {
border-top: 1px solid $border-gray-normal;
}
}
}
......
......@@ -243,22 +243,6 @@ ul.notes {
}
}
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.note-header {
.note-headline-light {
display: block;
}
.note-actions {
position: absolute;
right: 0;
top: 0;
}
}
}
}
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
......@@ -426,8 +410,22 @@ ul.notes {
}
.discussion-toggle-button {
padding: 0;
background-color: transparent;
border: 0;
line-height: 20px;
font-size: 13px;
transition: color 0.1s linear;
&:hover {
color: $gl-link-color;
}
&:focus {
text-decoration: underline;
outline: none;
color: $gl-link-color;
}
.fa {
margin-right: 3px;
......
......@@ -477,20 +477,6 @@ a.deploy-project-label {
}
}
.page-sidebar-pinned {
.project-stats .nav > li.right {
@media (min-width: $screen-lg-min) {
float: none;
}
}
.download-button {
@media (min-width: $screen-lg-min) {
margin-left: 0;
}
}
}
.project-stats {
font-size: 0;
text-align: center;
......@@ -587,9 +573,19 @@ pre.light-well {
display: flex;
flex-direction: column;
// Disable Flexbox for admin page
&.admin-projects {
display: block;
.project-row {
display: block;
}
}
.project-row {
display: flex;
align-items: center;
@include basic-list-stats;
}
h3 {
......@@ -746,6 +742,15 @@ pre.light-well {
}
}
.create-new-protected-branch-button {
@include dropdown-link;
width: 100%;
background-color: transparent;
border: 0;
text-align: left;
}
.protected-branches-list {
margin-bottom: 30px;
......
......@@ -3,25 +3,6 @@
*
*/
.navbar-nav {
li {
.badge.todos-pending-count {
position: inherit;
top: -6px;
margin-top: -5px;
font-weight: normal;
background: $todo-alert-blue;
margin-left: -17px;
font-size: 11px;
color: $white-light;
padding: 3px;
padding-top: 1px;
padding-bottom: 1px;
border-radius: 3px;
}
}
}
.todos-list > .todo {
// workaround because we cannot use border-colapse
border-top: 1px solid transparent;
......
......@@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
restricted_levels = params[:application_setting][:restricted_visibility_levels]
if restricted_levels.nil?
params[:application_setting][:restricted_visibility_levels] = []
else
restricted_levels.map! do |level|
level.to_i
end
end
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
......
class Admin::BackgroundJobsController < Admin::ApplicationController
def show
ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency]
end
end
......@@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def create
@label = Label.new(label_params)
@label.template = true
@label = Labels::CreateService.new(label_params).execute(template: true)
if @label.save
if @label.persisted?
redirect_to admin_labels_url, notice: "Label was created"
else
render :new
......@@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def update
if @label.update(label_params)
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
redirect_to admin_labels_path, notice: 'label was successfully updated.'
else
render :edit
......
......@@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController
end
def create
@label = @group.labels.create(label_params)
@label = Labels::CreateService.new(label_params).execute(group: group)
if @label.valid?
redirect_to group_labels_path(@group)
......@@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController
end
def update
if @label.update_attributes(label_params)
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
redirect_back_or_group_labels_path
else
render :edit
......
......@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_path_or_name(name)
Namespace.find_by_full_path(name)
end
end
end
class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
def show
@user = current_user
end
def unlink
provider = params[:provider]
current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
identity = current_user.identities.find_by(provider: provider)
return render_404 unless identity
if unlink_allowed?(provider)
identity.destroy
else
flash[:alert] = "You are not allowed to unlink your primary login account"
end
redirect_to profile_account_path
end
end
......@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
params.require(:user).permit(:notification_email)
params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
end
......@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
render json: BuildSerializer
.new(project: @project, user: @current_user)
.represent_status(@build)
end
def erase
......
......@@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
def create
@label = @project.labels.create(label_params)
@label = Labels::CreateService.new(label_params).execute(project: @project)
if @label.valid?
respond_to do |format|
......@@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController
end
def update
if @label.update_attributes(label_params)
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
render :edit
......
......@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
:ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
......@@ -97,31 +97,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
if @start_sha
compared_diff_version
else
......@@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
def pipeline_status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
def ci_environments_status
environments =
begin
......
......@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@pipeline)
end
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
......
......@@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
params[:wiki].slice(:title, :content, :format, :message)
params.require(:wiki).permit(:title, :content, :format, :message)
end
end
......@@ -6,45 +6,19 @@ class SearchController < ApplicationController
layout 'search'
def show
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
end
search_service = SearchService.new(current_user, params)
if params[:group_id].present?
@group = Group.find_by(id: params[:group_id])
@group = nil unless can?(current_user, :read_group, @group)
end
@project = search_service.project
@group = search_service.group
return if params[:search].blank?
@search_term = params[:search]
@scope = params[:scope]
@show_snippets = params[:snippets].eql? 'true'
@search_results =
if @project
unless %w(blobs notes issues merge_requests milestones wiki_blobs
commits).include?(@scope)
@scope = 'blobs'
end
Search::ProjectService.new(@project, current_user, params).execute
elsif @show_snippets
unless %w(snippet_blobs snippet_titles).include?(@scope)
@scope = 'snippet_blobs'
end
Search::SnippetService.new(current_user, params).execute
else
unless %w(projects issues merge_requests milestones).include?(@scope)
@scope = 'projects'
end
Search::GlobalService.new(current_user, params).execute
end
@search_objects = @search_results.objects(@scope, params[:page])
@scope = search_service.scope
@show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_objects = search_service.search_objects
check_single_commit_result
end
......
class GroupFinder
include Gitlab::Allowable
def initialize(current_user)
@current_user = current_user
end
def execute(*params)
group = Group.find_by(*params)
if can?(@current_user, :read_group, group)
group
else
nil
end
end
end
......@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project?
if project
label_ids << project.group.labels if project.group.present?
label_ids << project.labels
if project.group.present?
labels_table = Label.arel_table
label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
)
)
else
label_ids << project.labels
end
end
else
label_ids << Label.where(group_id: projects.group_ids)
......
......@@ -306,4 +306,8 @@ module ApplicationHelper
def active_when(condition)
'active' if condition
end
def show_user_callout?
cookies[:user_callout_dismissed] == 'true'
end
end
......@@ -76,5 +76,9 @@ module AuthHelper
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s)
end
extend self
end
......@@ -251,6 +251,21 @@ module IssuablesHelper
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, todo, is_collapsed)
{
todo_text: "Add todo",
mark_text: "Mark done",
todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id,
issuable_type: issuable.class.name.underscore,
url: namespace_project_todos_path(@project.namespace, @project),
delete_path: (dashboard_todo_path(todo) if todo),
placement: (is_collapsed ? 'left' : nil),
container: (is_collapsed ? 'body' : nil)
}
end
end
......@@ -19,8 +19,8 @@ module MilestonesHelper
end
end
def milestones_browse_issuables_path(milestone, type:)
opts = { milestone_title: milestone.title }
def milestones_browse_issuables_path(milestone, state: nil, type:)
opts = { milestone_title: milestone.title, state: state }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
......
......@@ -6,7 +6,8 @@ module NavHelper
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show')
current_path?('issues#show') ||
current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
......
......@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
(?<command>sidekiq.*\])
(?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
(?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x
def parse_sidekiq_ps(line)
......
......@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
def ipython_notebook?
text? && language&.name == 'Jupyter Notebook'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
elsif ipython_notebook?
'notebook'
elsif text?
'text'
else
......
......@@ -5,7 +5,7 @@ class Board < ActiveRecord::Base
validates :project, presence: true
def done_list
lists.merge(List.done).take
def closed_list
lists.merge(List.closed).take
end
end
......@@ -210,7 +210,7 @@ module Ci
end
def stuck?
builds.pending.any?(&:stuck?)
builds.pending.includes(:project).any?(&:stuck?)
end
def retryable?
......
......@@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base
end
end
def locking_enabled?
status_changed?
end
def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { label: 1, done: 2 }
enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base
# Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was)
gitlab_shell.add_namespace(repository_storage_path, full_path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
......@@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base
end
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
remove_exports!
......@@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base
def send_update_instructions
projects.each do |project|
project.send_move_instructions("#{path_was}/#{project.path}")
project.send_move_instructions("#{full_path_was}/#{project.path}")
end
end
......@@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base
old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash.
# We will remove it later async
new_path = "#{path}+#{id}+deleted"
new_path = "#{full_path}+#{id}+deleted"
if gitlab_shell.mv_namespace(repository_storage_path, path, new_path)
message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\""
Gitlab::AppLogger.info message
# Remove namespace directroy async with delay so
......
......@@ -37,6 +37,7 @@ class Note < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
has_one :system_note_metadata
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
......@@ -70,7 +71,9 @@ class Note < ActiveRecord::Base
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
scope :inc_relations_for_view, -> do
includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
end
scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
......
......@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help
"You need to configure JIRA before enabling this service. For more details
read the
[JIRA service documentation](#{help_page_url('project_services/jira')})."
[JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end
def title
......
......@@ -31,7 +31,7 @@ class PrometheusService < MonitoringService
def help
<<-MD.strip_heredoc
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
and `container_memory_usage_bytes` from the configured Prometheus server.
If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
......@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete?
memory_query = %{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024}
cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100}
memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
{
success: true,
......
......@@ -169,6 +169,9 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations.
where(user: user_ids).
group(:user_id).
......
......@@ -981,7 +981,13 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
merge_base(ancestor_id, descendant_id) == ancestor_id
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id)
else
merge_base_commit(ancestor_id, descendant_id) == ancestor_id
end
end
end
def empty_repo?
......
class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit merge confidentiality status label assignee cross_reference
title time_tracking branch milestone discussion task moved
].freeze
validates :note, presence: true
validates :action, inclusion: ICON_TYPES, allow_nil: true
belongs_to :note
end
......@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
......@@ -634,8 +635,10 @@ class User < ActiveRecord::Base
end
def fork_of(project)
links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)
links = ForkedProjectLink.where(
forked_from_project_id: project,
forked_to_project_id: personal_projects.unscope(:order)
)
if links.any?
links.first.forked_to_project
else
......
......@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
private
alias_method :build, :object
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
def detailed_status
build.detailed_status(request.user)
end
end
class BuildSerializer < BaseSerializer
entity BuildEntity
def represent_status(resource)
data = represent(resource, { only: [:status] })
data.fetch(:status, {})
end
end
......@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stop_action?
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
end
expose :environment_path do |environment|
namespace_project_environment_path(
environment.project.namespace,
......
......@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
expose :status do |pipeline, options|
StatusEntity.represent(
pipeline.detailed_status(request.user),
options)
end
expose :detailed_status, as: :status, with: StatusEntity
expose :duration
expose :finished_at
expose :stages, using: StageEntity
......@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
def detailed_status
pipeline.detailed_status(request.user)
end
end
......@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
end
def represent_status(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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