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: ...@@ -14,13 +14,15 @@ variables:
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" 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: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
- bundle --version - bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS' - '[ "$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' - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
stages: stages:
...@@ -39,14 +41,15 @@ stages: ...@@ -39,14 +41,15 @@ stages:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache: cache:
key: "knapsack" key: "knapsack"
paths: paths:
- knapsack/ - knapsack/
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
- knapsack/ - knapsack/
.use-db: &use-db .use-db: &use-db
services: services:
...@@ -61,17 +64,17 @@ stages: ...@@ -61,17 +64,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - 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 - 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" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
...@@ -81,28 +84,44 @@ stages: ...@@ -81,28 +84,44 @@ stages:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]} - 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 - 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)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
paths: paths:
- coverage/ - coverage/
- knapsack/ - knapsack/
- tmp/capybara/ - tmp/capybara/
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
<<: *knapsack-state <<: *knapsack-state
<<: *dedicated-runner <<: *dedicated-runner
stage: prepare stage: prepare
script: script:
- mkdir -p knapsack/ - mkdir -p knapsack/${CI_PROJECT_NAME}/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' - 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
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' - 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: setup-test-env:
<<: *use-db <<: *use-db
...@@ -122,20 +141,6 @@ setup-test-env: ...@@ -122,20 +141,6 @@ setup-test-env:
- public/assets - public/assets
- tmp/tests - 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 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack rspec 2 20: *rspec-knapsack
...@@ -287,14 +292,35 @@ rake karma: ...@@ -287,14 +292,35 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
lint-doc: docs:check:apilint:
image: "phusion/baseimage"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
image: "phusion/baseimage:latest" variables:
GIT_DEPTH: "3"
cache: {}
dependencies: []
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh - 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: bundler:check:
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
......
...@@ -2,6 +2,24 @@ ...@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 9.0.0 (2017-03-22)
- Fix inconsistent naming for services that delete things. !5803 (dixpac) - Fix inconsistent naming for services that delete things. !5803 (dixpac)
......
...@@ -314,9 +314,12 @@ request is as follows: ...@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash] organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch 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 but feel free to require more.
1. Your merge request needs at least 1 approval For instance if you're touching backend and frontend code, it's a good idea
1. You don't have to select any approvers 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 title should describe the change you want to make
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it. used to achieve it.
...@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted: ...@@ -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 1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][team]**. **approved by a [frontend maintainer][team]**.
1. If your merge request includes frontend and backend changes [^1], it must 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 1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][team] for a first review. 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 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 ...@@ -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 [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml [^1]: Please note that specs other than JavaScript specs are considered backend
changes are considered backend code if they include Ruby code other than just code.
pure HTML.
...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0' ...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0' gem 'rugged', '~> 0.25.1.1'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
...@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' ...@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false 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 # Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
......
...@@ -288,9 +288,9 @@ GEM ...@@ -288,9 +288,9 @@ GEM
rouge (~> 2.0) rouge (~> 2.0)
sanitize (~> 2.1.0) sanitize (~> 2.1.0)
stringex (~> 2.5.1) stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2) gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15) mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3) rugged (~> 0.25)
gon (6.1.0) gon (6.1.0)
actionpack (>= 3.0) actionpack (>= 3.0)
json json
...@@ -682,7 +682,7 @@ GEM ...@@ -682,7 +682,7 @@ GEM
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.1.10) rufus-scheduler (3.1.10)
rugged (0.24.0) rugged (0.25.1.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
...@@ -905,7 +905,7 @@ DEPENDENCIES ...@@ -905,7 +905,7 @@ DEPENDENCIES
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.8.6) google-api-client (~> 0.8.6)
grape (~> 0.19.0) grape (~> 0.19.0)
...@@ -987,7 +987,7 @@ DEPENDENCIES ...@@ -987,7 +987,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0) rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0) 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/ // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) { ...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16) const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16) const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) { 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); const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5; return cp >= tone1 && cp <= tone5;
}); });
...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) { ...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing // doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) { function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode); isSkinToneComboEmoji(emojiUnicode);
} }
...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) ...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) { function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false; let hasPersonEmoji = false;
let hasZwj = false; let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => { Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0); const cp = character.codePointAt(0);
if (cp === zwj) { if (cp === zwj) {
hasZwj = true; 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 @@ ...@@ -18,7 +18,7 @@
// Button does not change visibility. If button has icon - it changes chevron style. // Button does not change visibility. If button has icon - it changes chevron style.
// //
// %div.js-toggle-container // %div.js-toggle-container
// %a.js-toggle-button // %button.js-toggle-button
// %div.js-toggle-content // %div.js-toggle-content
// //
$('body').on('click', '.js-toggle-button', function(e) { $('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 @@ $(() => { ...@@ -79,7 +79,7 @@ $(() => {
resp.json().forEach((board) => { resp.json().forEach((board) => {
const list = Store.addList(board); const list = Store.addList(board);
if (list.type === 'done') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
} }
}); });
......
...@@ -50,9 +50,7 @@ export default { ...@@ -50,9 +50,7 @@ export default {
this.showDetail = false; this.showDetail = false;
}, },
showIssue(e) { showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase(); if (e.target.classList.contains('js-no-trigger')) return;
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
......
...@@ -84,20 +84,20 @@ import eventHub from '../eventhub'; ...@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }} #{{ issue.id }}
</span> </span>
<a <a
class="card-assignee has-tooltip" class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username" :href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name" :title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee" v-if="issue.assignee"
data-container="body"> data-container="body">
<img <img
class="avatar avatar-inline s20" class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar" :src="issue.assignee.avatar"
width="20" width="20"
height="20" height="20"
:alt="'Avatar for ' + issue.assignee.name" /> :alt="'Avatar for ' + issue.assignee.name" />
</a> </a>
<button <button
class="label color-label has-tooltip" class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels" v-for="label in issue.labels"
type="button" type="button"
v-if="showLabel(label)" v-if="showLabel(label)"
......
...@@ -48,7 +48,7 @@ import Vue from 'vue'; ...@@ -48,7 +48,7 @@ import Vue from 'vue';
template: ` template: `
<div <div
class="block list" class="block list"
v-if="list.type !== 'done'"> v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -10,7 +10,7 @@ class List { ...@@ -10,7 +10,7 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; 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.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
......
...@@ -45,7 +45,7 @@ import Cookies from 'js-cookie'; ...@@ -45,7 +45,7 @@ import Cookies from 'js-cookie';
}, },
shouldAddBlankState () { shouldAddBlankState () {
// Decide whether to add the blank state // 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 () { addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
...@@ -98,7 +98,7 @@ import Cookies from 'js-cookie'; ...@@ -98,7 +98,7 @@ import Cookies from 'js-cookie';
issueTo.removeLabel(listFrom.label); issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'done') { if (listTo.type === 'closed') {
issueLists.forEach((list) => { issueLists.forEach((list) => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
......
...@@ -33,12 +33,11 @@ export default Vue.component('pipelines-table', { ...@@ -33,12 +33,11 @@ export default Vue.component('pipelines-table', {
* @return {Object} * @return {Object}
*/ */
data() { data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const store = new PipelineStore(); const store = new PipelineStore();
return { return {
endpoint: pipelinesTableData.endpoint, endpoint: null,
helpPagePath: pipelinesTableData.helpPagePath, helpPagePath: null,
store, store,
state: store.state, state: store.state,
isLoading: false, isLoading: false,
...@@ -65,6 +64,8 @@ export default Vue.component('pipelines-table', { ...@@ -65,6 +64,8 @@ export default Vue.component('pipelines-table', {
* *
*/ */
beforeMount() { beforeMount() {
this.endpoint = this.$el.dataset.endpoint;
this.helpPagePath = this.$el.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); this.fetchPipelines();
......
...@@ -33,6 +33,8 @@ ...@@ -33,6 +33,8 @@
/* global ProjectShow */ /* global ProjectShow */
/* global Labels */ /* global Labels */
/* global Shortcuts */ /* global Shortcuts */
/* global Sidebar */
import Issue from './issue'; import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
...@@ -118,6 +120,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -118,6 +120,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'groups:milestones:show': case 'groups:milestones:show':
case 'dashboard:milestones:show': case 'dashboard:milestones:show':
new Milestone(); new Milestone();
new Sidebar();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new gl.Todos(); new gl.Todos();
......
...@@ -25,6 +25,12 @@ export default { ...@@ -25,6 +25,12 @@ export default {
}; };
}, },
computed: {
title() {
return 'Deploy to...';
},
},
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
...@@ -44,8 +50,11 @@ export default { ...@@ -44,8 +50,11 @@ export default {
template: ` template: `
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button <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" data-toggle="dropdown"
:title="title"
:aria-label="title"
:disabled="isLoading"> :disabled="isLoading">
<span> <span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
......
...@@ -9,13 +9,21 @@ export default { ...@@ -9,13 +9,21 @@ export default {
}, },
}, },
computed: {
title() {
return 'Open';
},
},
template: ` template: `
<a <a
class="btn external_url" class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl" :href="externalUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer nofollow"
title="Environment external URL"> :title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
`, `,
......
...@@ -5,6 +5,7 @@ import ExternalUrlComponent from './environment_external_url'; ...@@ -5,6 +5,7 @@ import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
/** /**
...@@ -22,6 +23,7 @@ export default { ...@@ -22,6 +23,7 @@ export default {
'stop-component': StopComponent, 'stop-component': StopComponent,
'rollback-component': RollbackComponent, 'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent, 'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
}, },
props: { props: {
...@@ -392,6 +394,14 @@ export default { ...@@ -392,6 +394,14 @@ export default {
return ''; 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. * Constructs folder URL based on the current location and the folder id.
* *
...@@ -496,13 +506,16 @@ export default { ...@@ -496,13 +506,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment" <external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> :external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment" <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
:stop-url="model.stop_path" :monitoring-url="monitoringUrl"/>
:service="service"/>
<terminal-button-component v-if="model && model.terminal_path" <terminal-button-component v-if="model && model.terminal_path"
:terminal-path="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" <rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :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 { ...@@ -25,6 +25,12 @@ export default {
}; };
}, },
computed: {
title() {
return 'Stop';
},
},
methods: { methods: {
onClick() { onClick() {
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
...@@ -45,10 +51,12 @@ export default { ...@@ -45,10 +51,12 @@ export default {
template: ` template: `
<button type="button" <button type="button"
class="btn stop-env-link" class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick" @click="onClick"
:disabled="isLoading" :disabled="isLoading"
title="Stop Environment"> :title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> <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> <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button> </button>
......
...@@ -14,12 +14,22 @@ export default { ...@@ -14,12 +14,22 @@ export default {
}, },
data() { data() {
return { terminalIconSvg }; return {
terminalIconSvg,
};
},
computed: {
title() {
return 'Terminal';
},
}, },
template: ` template: `
<a class="btn terminal-button" <a class="btn terminal-button has-tooltip"
title="Open web terminal" data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"> :href="terminalPath">
${terminalIconSvg} ${terminalIconSvg}
</a> </a>
......
...@@ -8,21 +8,31 @@ require('./filtered_search_token_keys'); ...@@ -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) // 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 tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = []; const tokens = [];
const tokenIndexes = []; // stores key+value for simple search
let lastToken = null; let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3; let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol; let tokenSymbol = symbol;
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue; tokenSymbol = tokenValue;
tokenValue = ''; tokenValue = '';
} }
tokens.push({ tokenIndex = `${key}:${tokenValue}`;
key,
value: tokenValue || '', // Prevent adding duplicates
symbol: tokenSymbol || '', if (tokenIndexes.indexOf(tokenIndex) === -1) {
}); tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return ''; return '';
}).replace(/\s{2,}/g, ' ').trim() || ''; }).replace(/\s{2,}/g, ' ').trim() || '';
......
const GROUP_LIMIT = 2;
import _ from 'underscore';
export default class GroupName { export default class GroupName {
constructor() { constructor() {
this.titleContainer = document.querySelector('.title'); this.titleContainer = document.querySelector('.title-container');
this.groups = document.querySelectorAll('.group-path'); this.title = document.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title'); this.groupTitle = document.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path');
this.toggle = null; this.toggle = null;
this.isHidden = false; this.isHidden = false;
this.init(); this.init();
} }
init() { init() {
if (this.groups.length > GROUP_LIMIT) { if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable'); this.groups[this.groups.length - 1].classList.remove('hidable');
this.addToggle(); this.toggleHandler();
window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
} }
this.render(); this.render();
} }
addToggle() { toggleHandler() {
const header = document.querySelector('.header-content'); 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 = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle'; this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...'; this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this)); this.toggle.addEventListener('click', this.toggleGroups.bind(this));
header.insertBefore(this.toggle, this.titleContainer); this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups(); 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() { toggleGroups() {
this.isHidden = !this.isHidden; this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden'); this.groupTitle.classList.toggle('is-hidden');
} }
render() { render() {
this.titleContainer.classList.remove('initializing'); this.title.classList.remove('initializing');
} }
} }
...@@ -6,23 +6,60 @@ var slice = [].slice; ...@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() { window.GroupsSelect = (function() {
function GroupsSelect() { function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) { $('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) { return function(i, select) {
var all_available, skip_groups; var all_available, skip_groups;
all_available = $(select).data('all-available'); const $select = $(select);
skip_groups = $(select).data('skip-groups') || []; all_available = $select.data('all-available');
return $(select).select2({ skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group", placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'), multiple: $select.hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
query: function(query) { ajax: {
var options = { all_available: all_available, skip_groups: skip_groups }; url: Api.buildUrl(Api.groupsPath),
return Api.groups(query.term, options, function(groups) { dataType: 'json',
var data; quietMillis: 250,
data = { transport: function (params) {
results: groups $.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) { initSelection: function(element, callback) {
var id; var id;
...@@ -34,19 +71,23 @@ window.GroupsSelect = (function() { ...@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() { formatResult: function() {
var args; var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args); return self.formatResult.apply(self, args);
}, },
formatSelection: function() { formatSelection: function() {
var args; var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; 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 // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) { escapeMarkup: function(m) {
return m; return m;
} }
}); });
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
}; };
})(this)); })(this));
} }
...@@ -65,5 +106,12 @@ window.GroupsSelect = (function() { ...@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name; 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; return GroupsSelect;
})(); })();
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
$(document).on('todo:toggle', function(e, count) { $(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count'); var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count)); $todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0); $todoPendingCount.toggleClass('hidden', count === 0);
}); });
...@@ -11,8 +11,9 @@ ...@@ -11,8 +11,9 @@
}); });
}; };
$(function() { $(document).on('init.scrolling-tabs', () => {
var $scrollingTabs = $('.scrolling-tabs'); const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs); hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() { $(window).off('resize.nav').on('resize.nav', function() {
......
...@@ -231,6 +231,22 @@ ...@@ -231,6 +231,22 @@
return upperCaseHeaders; 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. * Parses pagination object string values into numbers.
* *
......
...@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status'; ...@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status';
* Service for vue resouce and method need to be provided as props * Service for vue resouce and method need to be provided as props
* *
* @example * @example
* new poll({ * new Poll({
* resource: resource, * resource: resource,
* method: 'name', * method: 'name',
* data: {page: 1, scope: 'all'}, * data: {page: 1, scope: 'all'}, // optional
* successCallback: () => {}, * successCallback: () => {},
* errorCallback: () => {}, * errorCallback: () => {},
* notificationCallback: () => {}, // optional
* }).makeRequest(); * }).makeRequest();
* *
* this.service = new BoardsService(endpoint); * Usage in pipelines table with visibility lib:
* new poll({
* resource: this.service,
* method: 'get',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
* *
* 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 * 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header. * 2. Interval is provided by `Poll-Interval` header.
...@@ -34,6 +48,8 @@ export default class Poll { ...@@ -34,6 +48,8 @@ export default class Poll {
constructor(options = {}) { constructor(options = {}) {
this.options = options; this.options = options;
this.options.data = options.data || {}; this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback ||
function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL'; this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null; this.timeoutID = null;
...@@ -42,7 +58,7 @@ export default class Poll { ...@@ -42,7 +58,7 @@ export default class Poll {
checkConditions(response) { checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers); 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) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
...@@ -54,11 +70,14 @@ export default class Poll { ...@@ -54,11 +70,14 @@ export default class Poll {
} }
makeRequest() { 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) return resource[method](data)
.then(response => this.checkConditions(response)) .then(response => this.checkConditions(response))
.catch(error => errorCallback(error)); .catch(error => errorCallback(error));
} }
/** /**
...@@ -70,4 +89,12 @@ export default class Poll { ...@@ -70,4 +89,12 @@ export default class Poll {
this.canPoll = false; this.canPoll = false;
clearTimeout(this.timeoutID); clearTimeout(this.timeoutID);
} }
/**
* Restarts polling after it has been stoped
*/
restart() {
this.canPoll = true;
this.makeRequest();
}
} }
...@@ -370,4 +370,6 @@ $(function () { ...@@ -370,4 +370,6 @@ $(function () {
new Aside(); new Aside();
gl.utils.initTimeagoTimeout(); gl.utils.initTimeagoTimeout();
$(document).trigger('init.scrolling-tabs');
}); });
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
require('./breakpoints'); import CommitPipelinesTable from './commit/pipelines/pipelines_table';
require('./flash');
import './breakpoints';
import './flash';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -97,6 +99,13 @@ require('./flash'); ...@@ -97,6 +99,13 @@ require('./flash');
.off('click', this.clickTab); .off('click', this.clickTab);
} }
destroy() {
this.unbindEvents();
if (this.commitPipelinesTable) {
this.commitPipelinesTable.$destroy();
}
}
showTab(e) { showTab(e) {
e.preventDefault(); e.preventDefault();
this.activateTab($(e.target).data('action')); this.activateTab($(e.target).data('action'));
...@@ -128,12 +137,8 @@ require('./flash'); ...@@ -128,12 +137,8 @@ require('./flash');
this.expandViewContainer(); this.expandViewContainer();
} }
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
if (this.pipelinesLoaded) { this.resetViewContainer();
return; this.loadPipelines();
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
this.pipelinesLoaded = true;
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
...@@ -222,6 +227,18 @@ require('./flash'); ...@@ -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) { loadDiff(source) {
if (this.diffsLoaded) { if (this.diffsLoaded) {
return; return;
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
bindEvents() { bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', 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:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs); $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
......
...@@ -6,7 +6,7 @@ class ProtectedBranchDropdown { ...@@ -6,7 +6,7 @@ class ProtectedBranchDropdown {
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); 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.buildDropdown();
this.bindEvents(); this.bindEvents();
...@@ -46,7 +46,9 @@ class ProtectedBranchDropdown { ...@@ -46,7 +46,9 @@ class ProtectedBranchDropdown {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
} }
onClickCreateWildcard() { onClickCreateWildcard(e) {
e.preventDefault();
// Refresh the dropdown's data, which ends up calling `getProtectedBranches` // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(); this.$dropdown.data('glDropdown').selectRowAtIndex();
...@@ -69,7 +71,7 @@ class ProtectedBranchDropdown { ...@@ -69,7 +71,7 @@ class ProtectedBranchDropdown {
if (branchName) { if (branchName) {
this.$dropdownContainer this.$dropdownContainer
.find('.create-new-protected-branch code') .find('.js-create-new-protected-branch code')
.text(branchName); .text(branchName);
} }
......
...@@ -56,14 +56,15 @@ import Cookies from 'js-cookie'; ...@@ -56,14 +56,15 @@ import Cookies from 'js-cookie';
Sidebar.prototype.toggleTodo = function(e) { Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url; var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget); $this = $(e.currentTarget);
$todoLoading = $('.js-issuable-todo-loading');
$btnText = $('.js-issuable-todo-text', $this);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
if ($this.attr('data-delete-path')) { if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path')); url = "" + ($this.attr('data-delete-path'));
} else { } else {
url = "" + ($this.data('url')); url = "" + ($this.data('url'));
} }
$this.tooltip('hide');
return $.ajax({ return $.ajax({
url: url, url: url,
type: ajaxType, type: ajaxType,
...@@ -74,34 +75,44 @@ import Cookies from 'js-cookie'; ...@@ -74,34 +75,44 @@ import Cookies from 'js-cookie';
}, },
beforeSend: (function(_this) { beforeSend: (function(_this) {
return function() { return function() {
return _this.beforeTodoSend($this, $todoLoading); $('.js-issuable-todo').disable()
.addClass('is-loading');
}; };
})(this) })(this)
}).done((function(_this) { }).done((function(_this) {
return function(data) { return function(data) {
return _this.todoUpdateDone(data, $this, $btnText, $todoLoading); return _this.todoUpdateDone(data);
}; };
})(this)); })(this));
}; };
Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) { Sidebar.prototype.todoUpdateDone = function(data) {
$btn.disable(); const deletePath = data.delete_path ? data.delete_path : null;
return $todoLoading.removeClass('hidden'); const attrPrefix = deletePath ? 'mark' : 'todo';
}; const $todoBtns = $('.js-issuable-todo');
Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
$(document).trigger('todo:toggle', data.count); $(document).trigger('todo:toggle', data.count);
$btn.enable(); $todoBtns.each((i, el) => {
$todoLoading.addClass('hidden'); const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner');
if (data.delete_path != null) { $el.removeClass('is-loading')
$btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); .enable()
return $btnText.text($btn.data('mark-text')); .attr('aria-label', $el.data(`${attrPrefix}-text`))
} else { .attr('data-delete-path', deletePath)
$btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path'); .attr('title', $el.data(`${attrPrefix}-text`));
return $btnText.text($btn.data('todo-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) { Sidebar.prototype.sidebarDropdownLoading = function(e) {
...@@ -198,7 +209,7 @@ import Cookies from 'js-cookie'; ...@@ -198,7 +209,7 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.setSidebarHeight = function() { 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 $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop(); const diff = $navHeight - $(window).scrollTop();
if (diff > 0) { if (diff > 0) {
......
import Cookies from 'js-cookie'; 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_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 { export default class UserCallout {
constructor() { constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName); this.userCalloutBody = $('.user-callout');
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.init(); this.init();
} }
init() { init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg); $('.js-close-callout').on('click', e => this.dismissCallout(e));
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();
} }
} }
dismissCallout(e) { dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
} }
} }
......
...@@ -83,6 +83,7 @@ export default { ...@@ -83,6 +83,7 @@ export default {
:class="buttonClass" :class="buttonClass"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
data-container="body"
data-placement="top" data-placement="top"
:disabled="isLoading"> :disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/> <i :class="iconClass" aria-hidden="true"/>
......
...@@ -16,8 +16,12 @@ export default { ...@@ -16,8 +16,12 @@ export default {
}, },
}, },
mounted() {
$(document).trigger('init.scrolling-tabs');
},
template: ` template: `
<ul class="nav-links"> <ul class="nav-links scrolling-tabs">
<li <li
class="js-pipelines-tab-all" class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}"> :class="{ 'active': scope === 'all'}">
......
...@@ -21,6 +21,7 @@ export default { ...@@ -21,6 +21,7 @@ export default {
<li v-for="artifact in artifacts"> <li v-for="artifact in artifacts">
<a <a
rel="nofollow" rel="nofollow"
download
:href="artifact.path"> :href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span> <span>Download {{artifact.name}} artifacts</span>
......
...@@ -182,8 +182,14 @@ export default { ...@@ -182,8 +182,14 @@ export default {
<div :class="cssClass"> <div :class="cssClass">
<div <div
class="top-area" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> 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 <navigation-tabs
:scope="scope" :scope="scope"
:count="state.count" :count="state.count"
......
...@@ -3,8 +3,8 @@ const UI_LIMIT = 6; ...@@ -3,8 +3,8 @@ const UI_LIMIT = 6;
const SPREAD = '...'; const SPREAD = '...';
const PREV = 'Prev'; const PREV = 'Prev';
const NEXT = 'Next'; const NEXT = 'Next';
const FIRST = '<< First'; const FIRST = '« First';
const LAST = 'Last >>'; const LAST = 'Last »';
export default { export default {
props: { props: {
......
...@@ -362,3 +362,13 @@ ...@@ -362,3 +362,13 @@
width: 100%; width: 100%;
} }
} }
.btn-blank {
padding: 0;
background: transparent;
border: 0;
&:focus {
outline: 0;
}
}
...@@ -119,6 +119,46 @@ ...@@ -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,
.dropdown-menu-nav { .dropdown-menu-nav {
display: none; display: none;
...@@ -178,43 +218,7 @@ ...@@ -178,43 +218,7 @@
} }
a { a {
display: block; @include dropdown-link;
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-header { .dropdown-header {
......
...@@ -26,7 +26,7 @@ header { ...@@ -26,7 +26,7 @@ header {
padding: 0 16px; padding: 0 16px;
z-index: 100; z-index: 100;
margin-bottom: 0; margin-bottom: 0;
height: $header-height; min-height: $header-height;
background-color: $gray-light; background-color: $gray-light;
border: none; border: none;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -48,10 +48,10 @@ header { ...@@ -48,10 +48,10 @@ header {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 18px; font-size: 18px;
padding: 0; padding: 0;
margin: ($header-height - 28) / 2 0; margin: (($header-height - 28) / 2) 3px;
margin-left: 8px; margin-left: 8px;
height: 28px; height: 28px;
min-width: 28px; min-width: 32px;
line-height: 28px; line-height: 28px;
text-align: center; text-align: center;
...@@ -73,21 +73,29 @@ header { ...@@ -73,21 +73,29 @@ header {
background-color: $gray-light; background-color: $gray-light;
color: $gl-text-color; color: $gl-text-color;
.todos-pending-count { svg {
background: darken($todo-alert-blue, 10%); fill: $gl-text-color;
} }
} }
.fa-caret-down { .fa-caret-down {
font-size: 14px; 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 { .navbar-toggle {
color: $nav-toggle-gray; color: $nav-toggle-gray;
margin: 6px 0; margin: 5px 0;
border-radius: 0; border-radius: 0;
position: absolute;
right: -10px; right: -10px;
padding: 6px 10px; padding: 6px 10px;
...@@ -135,14 +143,12 @@ header { ...@@ -135,14 +143,12 @@ header {
} }
.header-content { .header-content {
display: flex;
justify-content: space-between;
position: relative; position: relative;
height: $header-height; min-height: $header-height;
padding-left: 30px; padding-left: 30px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
.dropdown-menu { .dropdown-menu {
margin-top: -5px; margin-top: -5px;
} }
...@@ -165,8 +171,7 @@ header { ...@@ -165,8 +171,7 @@ header {
} }
.group-name-toggle { .group-name-toggle {
margin: 0 5px; margin: 3px 5px;
vertical-align: sub;
} }
.group-title { .group-title {
...@@ -177,39 +182,32 @@ header { ...@@ -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 { .title {
position: relative; position: relative;
padding-right: 20px; padding-right: 20px;
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
max-width: 385px; line-height: 22px;
display: inline-block; display: inline-block;
line-height: $header-height;
font-weight: normal; font-weight: normal;
color: $gl-text-color; color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
&.initializing { &.wrap {
display: none; white-space: normal;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) {
max-width: 190px;
} }
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { &.initializing {
max-width: 428px; opacity: 0;
}
@media (min-width: $screen-lg-min) {
max-width: 685px;
} }
a { a {
...@@ -226,10 +224,10 @@ header { ...@@ -226,10 +224,10 @@ header {
border: transparent; border: transparent;
background: transparent; background: transparent;
position: absolute; position: absolute;
top: 2px;
right: 3px; right: 3px;
width: 12px; width: 12px;
line-height: 19px; line-height: 19px;
margin-top: (($header-height - 19) / 2);
padding: 0; padding: 0;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
...@@ -247,15 +245,12 @@ header { ...@@ -247,15 +245,12 @@ header {
} }
.navbar-collapse { .navbar-collapse {
float: right; flex: 0 0 auto;
border-top: none; border-top: none;
padding: 0;
@media (min-width: $screen-md-min) {
padding: 0;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
float: none; flex: 1 1 auto;
} }
} }
} }
...@@ -269,10 +264,30 @@ header { ...@@ -269,10 +264,30 @@ header {
} }
} }
.page-sidebar-pinned.right-sidebar-expanded { .navbar-nav {
@media (max-width: $screen-md-max) { li {
.header-content .title { .badge {
width: 300px; 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 @@ ...@@ -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 { @mixin bulleted-list {
> ul { > ul {
list-style-type: disc; list-style-type: disc;
......
...@@ -146,6 +146,10 @@ ...@@ -146,6 +146,10 @@
display: block; display: block;
} }
&.scrolling-tabs {
float: left;
}
li a { li a {
padding: 16px 15px 11px; padding: 16px 15px 11px;
} }
...@@ -476,3 +480,44 @@ ...@@ -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 @@ ...@@ -33,7 +33,7 @@
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter_collapsed_width;
} }
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @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; padding-right: $gutter_collapsed_width;
} }
} }
......
...@@ -240,8 +240,13 @@ ...@@ -240,8 +240,13 @@
font-size: (14px / $issue-boards-font-size) * 1em; font-size: (14px / $issue-boards-font-size) * 1em;
} }
.card-assignee {
margin-right: 5px;
}
.avatar { .avatar {
margin-left: 0; margin-left: 0;
margin-right: 0;
} }
} }
...@@ -296,7 +301,7 @@ ...@@ -296,7 +301,7 @@
} }
} }
.issue-boards-sidebar { .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
......
...@@ -142,7 +142,9 @@ ...@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
border-radius: $border-radius-default; border-radius: $border-radius-default;
margin-left: 5px; margin-left: 5px;
line-height: 1; font-size: $gl-font-size;
line-height: $gl-font-size;
outline: none;
&:hover { &:hover {
background-color: darken($gray-light, 10%); background-color: darken($gray-light, 10%);
......
...@@ -431,6 +431,21 @@ ...@@ -431,6 +431,21 @@
border-bottom: none; 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 // Mobile
@media (max-width: 480px) { @media (max-width: 480px) {
.diff-title { .diff-title {
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
.table.ci-table { .table.ci-table {
.environments-actions { .environments-actions {
min-width: 200px; min-width: 300px;
} }
.environments-commit, .environments-commit,
...@@ -222,3 +222,12 @@ ...@@ -222,3 +222,12 @@
stroke: $black; stroke: $black;
stroke-width: 1; stroke-width: 1;
} }
.environments-actions {
.external-url,
.monitoring-url,
.terminal-button,
.stop-env-link {
width: 38px;
}
}
...@@ -9,16 +9,15 @@ ...@@ -9,16 +9,15 @@
} }
} }
.group-row { .group-root-path {
.stats { max-width: 40vw;
float: right; overflow: hidden;
line-height: $list-text-height; text-overflow: ellipsis;
color: $gl-text-color; word-wrap: nowrap;
}
span { .group-row {
margin-right: 15px; @include basic-list-stats;
}
}
} }
.ldap-group-links { .ldap-group-links {
......
...@@ -243,6 +243,10 @@ ...@@ -243,6 +243,10 @@
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
} }
.hide-expanded {
display: none;
}
} }
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
...@@ -282,10 +286,11 @@ ...@@ -282,10 +286,11 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: center; text-align: center;
padding-bottom: 10px; margin-bottom: 10px;
color: $issuable-sidebar-color; color: $issuable-sidebar-color;
&:hover { &:hover,
&:hover .todo-undone {
color: $gl-text-color; color: $gl-text-color;
} }
...@@ -294,6 +299,10 @@ ...@@ -294,6 +299,10 @@
margin-top: 0; margin-top: 0;
} }
.todo-undone {
color: $gl-link-color;
}
.author { .author {
display: none; display: none;
} }
...@@ -582,3 +591,21 @@ ...@@ -582,3 +591,21 @@
opacity: 0; 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 @@ ...@@ -60,7 +60,17 @@
} }
.modify-merge-commit-link { .modify-merge-commit-link {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-text-color; color: $gl-text-color;
&:hover,
&:focus {
text-decoration: underline;
}
} }
.merge-param-checkbox { .merge-param-checkbox {
......
...@@ -52,66 +52,62 @@ ...@@ -52,66 +52,62 @@
} }
} }
.milestone-summary { .milestone-sidebar {
.milestone-stat { .gutter-toggle {
white-space: nowrap; margin-bottom: 10px;
margin-right: 10px; }
&.with-drilldown { .milestone-progress {
margin-right: 2px; .title {
padding-top: 5px;
} }
}
.remaining-days { .progress {
color: $orange-600; height: 6px;
margin: 0;
}
} }
.milestone-stats-and-buttons { .collapsed-milestone-date {
display: flex; font-size: 12px;
justify-content: flex-start; }
flex-wrap: wrap;
@media (min-width: $screen-xs-min) { .milestone-date {
justify-content: space-between; display: block;
flex-wrap: nowrap;
}
} }
.milestone-progress-buttons { .date-separator {
order: 1; line-height: 5px;
margin-top: 10px; }
@media (min-width: $screen-xs-min) { .remaining-days strong {
order: 2; font-weight: normal;
margin-top: 0; }
flex-shrink: 0;
}
.btn { .milestone-stat {
float: left; float: left;
margin-right: $btn-side-margin; margin-right: 14px;
}
&:last-child { .milestone-stat:last-child {
margin-right: 0; margin-right: 0;
}
}
} }
.milestone-stats { .milestone-progress {
order: 2; .sidebar-collapsed-icon {
width: 100%; clear: both;
padding: 7px 0; padding: 15px 5px 5px;
flex-shrink: 1;
@media (min-width: $screen-xs-min) { .progress {
// when displayed on one line stats go first, buttons second margin: 5px 0;
order: 1; }
} }
} }
.progress { .right-sidebar-collapsed & {
width: 100%; .reference {
margin: 15px 0; border-top: 1px solid $border-gray-normal;
}
} }
} }
......
...@@ -243,22 +243,6 @@ ul.notes { ...@@ -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 // Diff code in discussion view
.discussion-body .diff-file { .discussion-body .diff-file {
.file-title { .file-title {
...@@ -426,8 +410,22 @@ ul.notes { ...@@ -426,8 +410,22 @@ ul.notes {
} }
.discussion-toggle-button { .discussion-toggle-button {
padding: 0;
background-color: transparent;
border: 0;
line-height: 20px; line-height: 20px;
font-size: 13px; font-size: 13px;
transition: color 0.1s linear;
&:hover {
color: $gl-link-color;
}
&:focus {
text-decoration: underline;
outline: none;
color: $gl-link-color;
}
.fa { .fa {
margin-right: 3px; margin-right: 3px;
......
...@@ -477,20 +477,6 @@ a.deploy-project-label { ...@@ -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 { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
...@@ -587,9 +573,19 @@ pre.light-well { ...@@ -587,9 +573,19 @@ pre.light-well {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// Disable Flexbox for admin page
&.admin-projects {
display: block;
.project-row {
display: block;
}
}
.project-row { .project-row {
display: flex; display: flex;
align-items: center; align-items: center;
@include basic-list-stats;
} }
h3 { h3 {
...@@ -746,6 +742,15 @@ pre.light-well { ...@@ -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 { .protected-branches-list {
margin-bottom: 30px; margin-bottom: 30px;
......
...@@ -3,25 +3,6 @@ ...@@ -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 { .todos-list > .todo {
// workaround because we cannot use border-colapse // workaround because we cannot use border-colapse
border-top: 1px solid transparent; border-top: 1px solid transparent;
......
...@@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def application_setting_params 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] import_sources = params[:application_setting][:import_sources]
if import_sources.nil? if import_sources.nil?
params[:application_setting][:import_sources] = [] params[:application_setting][:import_sources] = []
......
class Admin::BackgroundJobsController < Admin::ApplicationController class Admin::BackgroundJobsController < Admin::ApplicationController
def show def show
ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) 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/) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency] @concurrency = Sidekiq.options[:concurrency]
end end
end end
...@@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController ...@@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController
end end
def create def create
@label = Label.new(label_params) @label = Labels::CreateService.new(label_params).execute(template: true)
@label.template = true
if @label.save if @label.persisted?
redirect_to admin_labels_url, notice: "Label was created" redirect_to admin_labels_url, notice: "Label was created"
else else
render :new render :new
...@@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController ...@@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController
end end
def update 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.' redirect_to admin_labels_path, notice: 'label was successfully updated.'
else else
render :edit render :edit
......
...@@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController
end end
def create def create
@label = @group.labels.create(label_params) @label = Labels::CreateService.new(label_params).execute(group: group)
if @label.valid? if @label.valid?
redirect_to group_labels_path(@group) redirect_to group_labels_path(@group)
...@@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController
end end
def update 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 redirect_back_or_group_labels_path
else else
render :edit render :edit
......
...@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController ...@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user) namespace.add_owner(current_user)
namespace namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_path_or_name(name) Namespace.find_by_full_path(name)
end end
end end
end end
class Profiles::AccountsController < Profiles::ApplicationController class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
def show def show
@user = current_user @user = current_user
end end
def unlink def unlink
provider = params[:provider] 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 redirect_to profile_account_path
end end
end end
...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:notification_email) params.require(:user).permit(:notification_email, :notified_of_own_activity)
end end
end end
...@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def status 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 end
def erase def erase
......
...@@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def create def create
@label = @project.labels.create(label_params) @label = Labels::CreateService.new(label_params).execute(project: @project)
if @label.valid? if @label.valid?
respond_to do |format| respond_to do |format|
...@@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def update 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) redirect_to namespace_project_labels_path(@project.namespace, @project)
else else
render :edit render :edit
......
...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, :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 :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :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 ...@@ -97,31 +97,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs def diffs
apply_diff_view_cookie! 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| respond_to do |format|
format.html { define_discussion_vars } format.html { define_discussion_vars }
format.json do 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 if @start_sha
compared_diff_version compared_diff_version
else else
...@@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -473,6 +473,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response render json: response
end end
def pipeline_status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
def ci_environments_status def ci_environments_status
environments = environments =
begin begin
......
...@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
end end
def status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@pipeline)
end
def stage def stage
@stage = pipeline.stage(params[:stage]) @stage = pipeline.stage(params[:stage])
return not_found unless @stage return not_found unless @stage
......
...@@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController
end end
def wiki_params def wiki_params
params[:wiki].slice(:title, :content, :format, :message) params.require(:wiki).permit(:title, :content, :format, :message)
end end
end end
...@@ -6,45 +6,19 @@ class SearchController < ApplicationController ...@@ -6,45 +6,19 @@ class SearchController < ApplicationController
layout 'search' layout 'search'
def show def show
if params[:project_id].present? search_service = SearchService.new(current_user, params)
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
end
if params[:group_id].present? @project = search_service.project
@group = Group.find_by(id: params[:group_id]) @group = search_service.group
@group = nil unless can?(current_user, :read_group, @group)
end
return if params[:search].blank? return if params[:search].blank?
@search_term = params[:search] @search_term = params[:search]
@scope = params[:scope] @scope = search_service.scope
@show_snippets = params[:snippets].eql? 'true' @show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_results = @search_objects = search_service.search_objects
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])
check_single_commit_result check_single_commit_result
end 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 ...@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project? if project?
if project if project
label_ids << project.group.labels if project.group.present? if project.group.present?
label_ids << project.labels 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 end
else else
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
......
...@@ -306,4 +306,8 @@ module ApplicationHelper ...@@ -306,4 +306,8 @@ module ApplicationHelper
def active_when(condition) def active_when(condition)
'active' if condition 'active' if condition
end end
def show_user_callout?
cookies[:user_callout_dismissed] == 'true'
end
end end
...@@ -76,5 +76,9 @@ module AuthHelper ...@@ -76,5 +76,9 @@ module AuthHelper
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end end
def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s)
end
extend self extend self
end end
...@@ -251,6 +251,21 @@ module IssuablesHelper ...@@ -251,6 +251,21 @@ module IssuablesHelper
end end
def selected_template(issuable) 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
end end
...@@ -19,8 +19,8 @@ module MilestonesHelper ...@@ -19,8 +19,8 @@ module MilestonesHelper
end end
end end
def milestones_browse_issuables_path(milestone, type:) def milestones_browse_issuables_path(milestone, state: nil, type:)
opts = { milestone_title: milestone.title } opts = { milestone_title: milestone.title, state: state }
if @project if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
......
...@@ -6,7 +6,8 @@ module NavHelper ...@@ -6,7 +6,8 @@ module NavHelper
current_path?('merge_requests#builds') || current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') || current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') || current_path?('merge_requests#pipelines') ||
current_path?('issues#show') current_path?('issues#show') ||
current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed" "page-gutter right-sidebar-collapsed"
else else
......
...@@ -3,9 +3,9 @@ module SidekiqHelper ...@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+ (?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+ (?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+ (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
(?<start>.+)\s+ (?<start>.+?)\s+
(?<command>sidekiq.*\]) (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x \z/x
def parse_sidekiq_ps(line) def parse_sidekiq_ps(line)
......
...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator ...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG' text? && language && language.name == 'SVG'
end end
def ipython_notebook?
text? && language&.name == 'Jupyter Notebook'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator ...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end end
elsif image? || svg? elsif image? || svg?
'image' 'image'
elsif ipython_notebook?
'notebook'
elsif text? elsif text?
'text' 'text'
else else
......
...@@ -5,7 +5,7 @@ class Board < ActiveRecord::Base ...@@ -5,7 +5,7 @@ class Board < ActiveRecord::Base
validates :project, presence: true validates :project, presence: true
def done_list def closed_list
lists.merge(List.done).take lists.merge(List.closed).take
end end
end end
...@@ -210,7 +210,7 @@ module Ci ...@@ -210,7 +210,7 @@ module Ci
end end
def stuck? def stuck?
builds.pending.any?(&:stuck?) builds.pending.includes(:project).any?(&:stuck?)
end end
def retryable? def retryable?
......
...@@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base
end end
end end
def locking_enabled?
status_changed?
end
def before_sha def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA pipeline.before_sha || Gitlab::Git::BLANK_SHA
end end
......
...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base ...@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board belongs_to :board
belongs_to :label belongs_to :label
enum list_type: { label: 1, done: 2 } enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label? validates :label, :position, presence: true, if: :label?
......
...@@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base ...@@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base
# Move the namespace directory in all storages paths used by member projects # Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path| repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it # 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) unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{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 # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
...@@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base ...@@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base
end end
end end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
Gitlab::PagesTransfer.new.rename_namespace(path_was, path) Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
remove_exports! remove_exports!
...@@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base ...@@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base
def send_update_instructions def send_update_instructions
projects.each do |project| projects.each do |project|
project.send_move_instructions("#{path_was}/#{project.path}") project.send_move_instructions("#{full_path_was}/#{project.path}")
end end
end end
...@@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base ...@@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base
old_repository_storage_paths.each do |repository_storage_path| old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash. # Move namespace directory into trash.
# We will remove it later async # 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) if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\""
Gitlab::AppLogger.info message Gitlab::AppLogger.info message
# Remove namespace directroy async with delay so # Remove namespace directroy async with delay so
......
...@@ -37,6 +37,7 @@ class Note < ActiveRecord::Base ...@@ -37,6 +37,7 @@ class Note < ActiveRecord::Base
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy has_many :events, as: :target, dependent: :destroy
has_one :system_note_metadata
delegate :gfm_reference, :local_reference, to: :noteable delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
...@@ -70,7 +71,9 @@ class Note < ActiveRecord::Base ...@@ -70,7 +71,9 @@ class Note < ActiveRecord::Base
scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(: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 :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
......
...@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService ...@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help def help
"You need to configure JIRA before enabling this service. For more details "You need to configure JIRA before enabling this service. For more details
read the read the
[JIRA service documentation](#{help_page_url('project_services/jira')})." [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end end
def title def title
......
...@@ -31,7 +31,7 @@ class PrometheusService < MonitoringService ...@@ -31,7 +31,7 @@ class PrometheusService < MonitoringService
def help def help
<<-MD.strip_heredoc <<-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. 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) If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
...@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService ...@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug) def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete? 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} 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="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100} 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, success: true,
......
...@@ -169,6 +169,9 @@ class ProjectTeam ...@@ -169,6 +169,9 @@ class ProjectTeam
# Lookup only the IDs we need # Lookup only the IDs we need
user_ids = user_ids - access.keys user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations. users_access = project.project_authorizations.
where(user: user_ids). where(user: user_ids).
group(:user_id). group(:user_id).
......
...@@ -981,7 +981,13 @@ class Repository ...@@ -981,7 +981,13 @@ class Repository
end end
def is_ancestor?(ancestor_id, descendant_id) 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 end
def empty_repo? 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 ...@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false default_value_for :hide_no_password, false
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
...@@ -634,8 +635,10 @@ class User < ActiveRecord::Base ...@@ -634,8 +635,10 @@ class User < ActiveRecord::Base
end end
def fork_of(project) 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? if links.any?
links.first.forked_to_project links.first.forked_to_project
else else
......
...@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity ...@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
private private
alias_method :build, :object
def path_to(route, build) def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) send("#{route}_path", build.project.namespace, build.project, build)
end end
def detailed_status
build.detailed_status(request.user)
end
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 ...@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action? 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| expose :environment_path do |environment|
namespace_project_environment_path( namespace_project_environment_path(
environment.project.namespace, environment.project.namespace,
......
...@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity ...@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end end
expose :details do expose :details do
expose :status do |pipeline, options| expose :detailed_status, as: :status, with: StatusEntity
StatusEntity.represent(
pipeline.detailed_status(request.user),
options)
end
expose :duration expose :duration
expose :finished_at expose :finished_at
expose :stages, using: StageEntity expose :stages, using: StageEntity
...@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity ...@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? && pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline) can?(request.user, :update_pipeline, pipeline)
end end
def detailed_status
pipeline.detailed_status(request.user)
end
end end
...@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer ...@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts) super(resource, opts)
end end
end end
def represent_status(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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