Commit ae2df96b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'rc/ce-to-ee-monday' into 'master'

CE Upstream - Monday

Closes gitlab-ce#29897

See merge request !1517
parents e56f1dc3 376fc805
......@@ -15,6 +15,8 @@ variables:
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m"
......@@ -23,7 +25,7 @@ before_script:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS'
- retry gem install knapsack
- retry gem install knapsack fog-aws mime-types
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
- '[ "$SETUP_DB" != "true" ] || bundle exec rake geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate'
......@@ -43,14 +45,15 @@ stages:
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache:
key: "knapsack"
paths:
- knapsack/
- knapsack/
artifacts:
expire_in: 31d
paths:
- knapsack/
- knapsack/
.use-db: &use-db
services:
......@@ -66,17 +69,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
when: always
paths:
- coverage/
- knapsack/
- tmp/capybara/
- coverage/
- knapsack/
- tmp/capybara/
.spinach-knapsack: &spinach-knapsack
stage: test
......@@ -86,28 +89,44 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
paths:
- coverage/
- knapsack/
- tmp/capybara/
- coverage/
- knapsack/
- tmp/capybara/
# Prepare and merge knapsack tests
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: prepare
script:
- mkdir -p knapsack/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
- mkdir -p knapsack/${CI_PROJECT_NAME}/
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
setup-test-env:
<<: *use-db
......@@ -127,20 +146,6 @@ setup-test-env:
- public/assets
- tmp/tests
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
......@@ -266,6 +271,7 @@ rake karma:
BABEL_ENV: "coverage"
script:
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
name: coverage-javascript
expire_in: 31d
......@@ -297,7 +303,7 @@ bundler:audit:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update"
- "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
migration paths:
stage: test
......@@ -331,6 +337,7 @@ coverage:
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
artifacts:
name: coverage
expire_in: 31d
......
......@@ -255,7 +255,7 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0"
# Sentry integration
gem 'sentry-raven', '~> 2.0.0'
gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
......@@ -268,15 +268,14 @@ end
group :development do
gem 'foreman', '~> 0.78.0'
gem 'brakeman', '~> 3.4.0', require: false
gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.2.0', require: false
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0'
# Better errors handler
gem 'better_errors', '~> 1.0.1'
gem 'better_errors', '~> 2.1.0'
gem 'binding_of_caller', '~> 0.7.2'
# thin instead webrick
......@@ -308,7 +307,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
gem 'spring', '~> 1.7.0'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
......@@ -316,8 +315,8 @@ group :development, :test do
gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '0.12.0', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
......@@ -334,7 +333,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.21.0'
gem 'webmock', '~> 1.24.0'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
......
......@@ -83,19 +83,20 @@ GEM
base32 (0.3.2)
bcrypt (3.1.11)
benchmark-ips (2.3.0)
better_errors (1.0.1)
better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.4.1)
brakeman (3.6.1)
browser (2.2.0)
builder (3.2.3)
bullet (5.2.0)
bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0)
......@@ -109,7 +110,7 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara-screenshot (1.0.11)
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.11.2)
......@@ -125,7 +126,7 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1)
......@@ -224,7 +225,9 @@ GEM
multi_json
ffaker (2.4.0)
ffi (1.9.10)
flay (2.6.1)
flay (2.8.1)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flowdock (0.7.1)
......@@ -375,6 +378,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
hashdiff (0.3.2)
hashie (3.5.5)
health_check (2.6.0)
rails (>= 4.0)
......@@ -554,6 +558,7 @@ GEM
activerecord (>= 4.0, < 5.1)
parser (2.4.0.0)
ast (~> 2.2)
path_expander (1.0.1)
pg (0.18.4)
poltergeist (1.9.0)
capybara (~> 2.1)
......@@ -568,14 +573,14 @@ GEM
premailer-rails (1.9.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
pry (0.10.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (3.4.1)
pry-byebug (3.4.2)
byebug (~> 9.0)
pry (~> 0.10)
pry-rails (0.3.4)
pry-rails (0.3.5)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.5)
......@@ -707,7 +712,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.2)
ruby_parser (3.8.4)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -736,10 +741,10 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
sentry-raven (2.0.2)
faraday (>= 0.7.6, < 0.10.x)
sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.7.0)
sexp_processor (4.8.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
......@@ -760,7 +765,7 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
simplecov (0.12.0)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
......@@ -777,7 +782,8 @@ GEM
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
spring (1.7.2)
spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
......@@ -849,14 +855,10 @@ GEM
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
webmock (1.21.0)
webmock (1.24.6)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.9)
rails (>= 3.2.0)
websocket-driver (0.6.3)
......@@ -891,12 +893,12 @@ DEPENDENCIES
babosa (~> 1.0.2)
base32 (~> 0.3.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 1.0.1)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.4.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
bullet (~> 5.2.0)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
......@@ -926,7 +928,7 @@ DEPENDENCIES
factory_girl_rails (~> 4.7.0)
faraday_middleware-aws-signers-v4
ffaker (~> 2.4)
flay (~> 2.6.1)
flay (~> 2.8.0)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-google (~> 0.5)
......@@ -1036,18 +1038,18 @@ DEPENDENCIES
scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
sentry-raven (~> 2.0.0)
sentry-raven (~> 2.4.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
......@@ -1068,8 +1070,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
webmock (~> 1.24.0)
webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1)
......
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ee/commits/master)
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript
## Canonical source
The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
......
......@@ -94,7 +94,7 @@ $(() => {
resp.json().forEach((board) => {
const list = Store.addList(board);
if (list.type === 'done') {
if (list.type === 'closed') {
list.position = Infinity;
}
});
......
......@@ -14,6 +14,8 @@ export default {
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
},
destroyed() {
this.filteredSearch.cleanup();
......
......@@ -53,7 +53,7 @@ import Vue from 'vue';
template: `
<div
class="block list"
v-if="list.type !== 'done'">
v-if="list.type !== 'closed'">
<button
class="btn btn-default btn-block"
type="button"
......
......@@ -29,6 +29,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
[].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el);
});
this.filteredSearchInput.value = '';
}
updateTokens() {
......
......@@ -10,7 +10,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1;
this.loading = true;
this.loadingMore = false;
......
......@@ -50,7 +50,7 @@ import Cookies from 'js-cookie';
},
shouldAddBlankState () {
// Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'done')[0]);
return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -103,7 +103,7 @@ import Cookies from 'js-cookie';
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'done') {
if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
/* eslint-disable no-new*/
/* global Flash */
import Vue from 'vue';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
import eventHub from '../../vue_pipelines_index/event_hub';
import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
......@@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('pipelines-table', {
components: {
'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState,
'empty-state': EmptyState,
},
/**
......@@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', {
return {
endpoint: pipelinesTableData.endpoint,
helpPagePath: pipelinesTableData.helpPagePath,
store,
state: store.state,
isLoading: false,
hasError: false,
};
},
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
shouldRenderEmptyState() {
return !this.state.pipelines.length && !this.isLoading;
},
},
/**
* When the component is about to be mounted, tell the service to fetch the data
*
......@@ -80,26 +94,25 @@ export default Vue.component('pipelines-table', {
this.isLoading = false;
})
.catch(() => {
this.hasError = true;
this.isLoading = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
},
template: `
<div class="pipelines">
<div class="content-list pipelines">
<div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.pipelines.length === 0">
<h2 class="blank-state-title js-blank-state-title">
No pipelines to show
</h2>
</div>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div class="table-holder pipelines"
<div class="table-holder"
v-if="!isLoading && state.pipelines.length > 0">
<pipelines-table-component
:pipelines="state.pipelines"
......
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/from';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
......
export default {
props: {
count: {
type: Number,
required: true,
},
},
template: `
<span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
aria-hidden="true"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
`,
};
......@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
......
......@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
......
......@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<div>
<div class="events-description">
{{ stage.description }}
<span v-if="items.length === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
......
......@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
......
......@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
......
......@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
......
......@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
<limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
......
......@@ -2,6 +2,7 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component');
require('./components/stage_issue_component');
......@@ -130,5 +131,6 @@ $(() => {
});
// Register global components
Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
......@@ -33,11 +33,7 @@ class Diff {
handleClickUnfold(e) {
const $target = $(e.target);
// current babel config relies on iterators implementation, so we cannot simply do:
// const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const ref = this.lineNumbers($target.parent());
const oldLineNumber = ref[0];
const newLineNumber = ref[1];
const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom');
let since;
......@@ -105,10 +101,11 @@ class Diff {
}
lineNumbers(line) {
if (!line.children().length) {
const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
return [0, 0];
}
return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
highlightSelectedLine() {
......
......@@ -56,10 +56,12 @@ require('../window')(function(w){
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
}
};
});
......
const GROUP_LIMIT = 2;
import _ from 'underscore';
export default class GroupName {
constructor() {
this.titleContainer = document.querySelector('.title');
this.groups = document.querySelectorAll('.group-path');
this.titleContainer = document.querySelector('.title-container');
this.title = document.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
if (this.groups.length > GROUP_LIMIT) {
if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
this.addToggle();
this.toggleHandler();
window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
addToggle() {
const header = document.querySelector('.header-content');
toggleHandler() {
if (this.titleWidth > this.titleContainer.offsetWidth) {
if (!this.toggle) this.createToggle();
this.showToggle();
} else if (this.toggle) {
this.hideToggle();
}
}
createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
header.insertBefore(this.toggle, this.titleContainer);
this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
showToggle() {
this.title.classList.add('wrap');
this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('is-hidden');
}
hideToggle() {
this.title.classList.remove('wrap');
this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
}
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
this.titleContainer.classList.remove('initializing');
this.title.classList.remove('initializing');
}
}
......@@ -36,20 +36,21 @@ export default class Poll {
this.options.data = options.data || {};
this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
this.canPoll = true;
}
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader];
if (pollInterval > 0 && response.status === httpStatusCodes.OK) {
this.options.successCallback(response);
setTimeout(() => {
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
}, pollInterval);
} else {
this.options.successCallback(response);
}
this.options.successCallback(response);
}
makeRequest() {
......@@ -59,4 +60,14 @@ export default class Poll {
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
/**
* Stops the polling recursive chain
* and guarantees if the timeout is already running it won't make another request by
* cancelling the previously established timeout.
*/
stop() {
this.canPoll = false;
clearTimeout(this.timeoutID);
}
}
......@@ -127,9 +127,6 @@ require('./flash');
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: 0,
});
} else if (action === 'pipelines') {
if (this.pipelinesLoaded) {
return;
......
......@@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
......
......@@ -200,7 +200,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $('body').scrollTop();
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
......
import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg';
export default {
props: {
helpPagePath: {
type: String,
required: true,
},
},
template: `
<div class="row empty-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesEmptyStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>Build with confidence</h4>
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
</a>
</div>
</div>
</div>
`,
};
import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg';
export default {
template: `
<div class="row empty-state js-pipelines-error-state">
<div class="col-xs-12">
<div class="svg-content">
${pipelinesErrorStateSVG}
</div>
</div>
<div class="col-xs-12 text-center">
<div class="text-content">
<h4>The API failed to fetch the pipelines.</h4>
</div>
</div>
</div>
`,
};
export default {
props: {
newPipelinePath: {
type: String,
required: true,
},
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
ciLintPath: {
type: String,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
},
template: `
<div class="nav-controls">
<a
v-if="canCreatePipeline"
:href="newPipelinePath"
class="btn btn-create">
Run Pipeline
</a>
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a>
<a
:href="ciLintPath"
class="btn btn-default">
CI Lint
</a>
</div>
`,
};
export default {
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
required: true,
},
},
template: `
<ul class="nav-links">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
<a :href="paths.allPath">
All
<span class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li class="js-pipelines-tab-pending"
:class="{ 'active': scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span class="badge">
{{count.pending}}
</span>
</a>
</li>
<li class="js-pipelines-tab-running"
:class="{ 'active': scope === 'running'}">
<a :href="paths.runningPath">
Running
<span class="badge">
{{count.running}}
</span>
</a>
</li>
<li class="js-pipelines-tab-finished"
:class="{ 'active': scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
<span class="badge">
{{count.finished}}
</span>
</a>
</li>
<li class="js-pipelines-tab-branches"
:class="{ 'active': scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li class="js-pipelines-tab-tags"
:class="{ 'active': scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
`,
};
......@@ -4,23 +4,19 @@ import PipelinesComponent from './pipelines';
import '../vue_shared/vue_resource_interceptor';
$(() => new Vue({
el: document.querySelector('.vue-pipelines-index'),
el: document.querySelector('#pipelines-list-vue'),
data() {
const project = document.querySelector('.pipelines');
const store = new PipelinesStore();
return {
store,
endpoint: project.dataset.url,
};
},
components: {
'vue-pipelines': PipelinesComponent,
},
template: `
<vue-pipelines
:endpoint="endpoint"
:store="store" />
<vue-pipelines :store="store" />
`,
}));
/* global Flash */
/* eslint-disable no-new */
import '~/flash';
import Vue from 'vue';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
import EmptyState from './components/empty_state';
import ErrorState from './components/error_state';
import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls';
export default {
props: {
endpoint: {
type: String,
required: true,
},
store: {
type: Object,
required: true,
......@@ -23,17 +19,109 @@ export default {
components: {
'gl-pagination': TablePaginationComponent,
'pipelines-table-component': PipelinesTableComponent,
'empty-state': EmptyState,
'error-state': ErrorState,
'navigation-tabs': NavigationTabs,
'navigation-controls': NavigationControls,
},
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return {
endpoint: pipelinesData.endpoint,
cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
pageRequest: false,
isLoading: false,
hasError: false,
};
},
computed: {
canCreatePipelineParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
* and none is returned.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
(this.scope === 'all' || this.scope === null);
},
/**
* When a specific scope does not have pipelines we render a message.
*
* @return {Boolean}
*/
shouldRenderNoPipelinesMessage() {
return !this.isLoading &&
!this.hasError &&
!this.state.pipelines.length &&
this.scope !== 'all' &&
this.scope !== null;
},
shouldRenderTable() {
return !this.hasError &&
!this.isLoading && this.state.pipelines.length;
},
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
},
},
created() {
this.service = new PipelinesService(this.endpoint);
......@@ -69,7 +157,7 @@ export default {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
this.pageRequest = true;
this.isLoading = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
......@@ -81,41 +169,72 @@ export default {
this.store.storePagination(response.headers);
})
.then(() => {
this.pageRequest = false;
this.isLoading = false;
})
.catch(() => {
this.pageRequest = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
this.hasError = true;
this.isLoading = false;
});
},
},
template: `
<div>
<div class="pipelines realtime-loading" v-if="pageRequest">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!pageRequest && state.pipelines.length === 0">
<h2 class="blank-state-title js-blank-state-title">
No pipelines to show
</h2>
template: `
<div :class="cssClass">
<div
class="top-area"
v-if="!isLoading && !shouldRenderEmptyState">
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths" />
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " />
</div>
<div class="table-holder" v-if="!pageRequest && state.pipelines.length">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
<div class="content-list pipelines">
<div
class="realtime-loading"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="blank-state blank-state-no-icon"
v-if="shouldRenderNoPipelinesMessage">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
</div>
<gl-pagination
v-if="shouldRenderPagination"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"/>
</div>
<gl-pagination
v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
:pagenum="pagenum"
:change="change"
:count="state.count.all"
:pageInfo="state.pageInfo"
>
</gl-pagination>
</div>
`,
};
......@@ -211,3 +211,11 @@ label {
color: $gl-text-color;
}
}
@media(max-width: $screen-xs-max) {
.remember-me {
.remember-me-checkbox {
margin-top: 0;
}
}
}
......@@ -26,7 +26,7 @@ header {
padding: 0 16px;
z-index: 100;
margin-bottom: 0;
height: $header-height;
min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
......@@ -85,7 +85,7 @@ header {
.navbar-toggle {
color: $nav-toggle-gray;
margin: 6px 0;
margin: 7px 0;
border-radius: 0;
position: absolute;
right: -10px;
......@@ -135,12 +135,14 @@ header {
}
.header-content {
display: flex;
justify-content: space-between;
position: relative;
height: $header-height;
min-height: $header-height;
padding-left: 30px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
@media (max-width: $screen-sm-max) {
padding-right: 20px;
}
.dropdown-menu {
......@@ -165,8 +167,7 @@ header {
}
.group-name-toggle {
margin: 0 5px;
vertical-align: sub;
margin: 3px 5px;
}
.group-title {
......@@ -177,39 +178,32 @@ header {
}
}
.title-container {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
padding-top: (($header-height - 19) / 2);
overflow: hidden;
}
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
max-width: 385px;
line-height: 22px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
&.initializing {
display: none;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
&.wrap {
white-space: normal;
}
@media (max-width: $screen-xs-max) {
max-width: 190px;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
max-width: 428px;
}
@media (min-width: $screen-lg-min) {
max-width: 685px;
&.initializing {
opacity: 0;
}
a {
......@@ -226,10 +220,10 @@ header {
border: transparent;
background: transparent;
position: absolute;
top: 2px;
right: 3px;
width: 12px;
line-height: 19px;
margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
......@@ -247,7 +241,7 @@ header {
}
.navbar-collapse {
float: right;
flex: 0 0 auto;
border-top: none;
@media (min-width: $screen-md-min) {
......@@ -255,7 +249,7 @@ header {
}
@media (max-width: $screen-xs-max) {
float: none;
flex: 1 1 auto;
}
}
}
......@@ -269,14 +263,6 @@ header {
}
}
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.header-content .title {
width: 300px;
}
}
}
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
......
......@@ -416,14 +416,16 @@
.page-with-layout-nav {
.right-sidebar {
top: ($header-height * 2) + 2;
top: ($header-height + 1) * 2;
}
.build-sidebar {
top: ($header-height * 3) + 3;
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3;
&.affix {
top: 0;
&.affix {
top: 0;
}
}
}
}
......
......@@ -176,6 +176,10 @@ summary {
&.panel-without-border {
border: 0;
}
&.panel-without-margin {
margin: 0;
}
}
.panel-succes .panel-heading,
......
......@@ -366,9 +366,3 @@
right: 0;
margin-top: -17px;
}
@media (min-width: $screen-md-min) {
.sub-nav.build {
width: calc(100% + #{$gutter_width});
}
}
......@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
line-height: 1;
font-size: $gl-font-size;
line-height: $gl-font-size;
outline: none;
&:hover {
background-color: darken($gray-light, 10%);
......
......@@ -18,7 +18,10 @@
.environments-container {
.table-holder {
width: 100%;
overflow: auto;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
}
.table.ci-table {
......
......@@ -9,6 +9,13 @@
}
}
.group-root-path {
max-width: 40vw;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: nowrap;
}
.content-list .group-name {
font-weight: 600;
color: $pages-group-name-color;
......
......@@ -243,22 +243,6 @@ ul.notes {
}
}
.page-sidebar-pinned.right-sidebar-expanded {
@media (max-width: $screen-md-max) {
.note-header {
.note-headline-light {
display: block;
}
.note-actions {
position: absolute;
right: 0;
top: 0;
}
}
}
}
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
......
......@@ -2,6 +2,7 @@
.realtime-loading {
font-size: 40px;
text-align: center;
margin: 0 auto;
}
.stage {
......@@ -13,9 +14,16 @@
white-space: nowrap;
}
.empty-state {
margin: 5% auto 0;
}
.table-holder {
width: 100%;
overflow: auto;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
}
.commit-title {
......@@ -99,8 +107,6 @@
@media (max-width: $screen-md-max) {
.content-list {
&.pipelines,
&.environments-container,
&.builds-content-list {
width: 100%;
overflow: auto;
......
......@@ -477,20 +477,6 @@ a.deploy-project-label {
}
}
.page-sidebar-pinned {
.project-stats .nav > li.right {
@media (min-width: $screen-lg-min) {
float: none;
}
}
.download-button {
@media (min-width: $screen-lg-min) {
margin-left: 0;
}
}
}
.project-stats {
font-size: 0;
text-align: center;
......@@ -582,54 +568,55 @@ pre.light-well {
/*
* Projects list rendered on dashboard and user page
*/
.projects-list {
@include basic-list;
display: flex;
flex-direction: column;
.project-row {
border-color: $white-normal;
.project-full-name {
@include str-truncated;
display: flex;
align-items: center;
}
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
h3 {
font-size: $gl-font-size;
}
.controls {
line-height: $list-text-height;
a {
color: $gl-text-color;
}
.badge {
@media (max-width: $screen-xs-max) {
display: none;
}
}
.avatar-container,
.controls {
flex: 0 0 auto;
}
a:hover {
text-decoration: none;
}
.avatar-container {
align-self: flex-start;
}
> span {
margin-left: 10px;
}
.project-details {
min-width: 0;
svg {
position: relative;
top: 2px;
}
p,
.commit-row-message {
@include str-truncated(100%);
margin-bottom: 0;
}
}
.description p {
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
.controls {
margin-left: auto;
}
.bottom {
padding-top: $gl-padding;
padding-bottom: 0;
.ci-status-link {
display: inline-block;
line-height: 17px;
vertical-align: middle;
&:hover {
text-decoration: none;
}
}
}
......
......@@ -47,6 +47,7 @@
.todo-avatar,
.todo-actions {
@include transition(opacity);
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
......@@ -67,21 +68,34 @@
flex: 0 1 100%;
min-width: 0;
}
}
.todos-list > .todo.todo-pending.done-reversible {
background-color: $gray-light;
&.todo-pending.done-reversible {
background-color: $white-light;
&:hover {
border-color: $border-color;
}
&:hover {
border-color: $white-dark;
background-color: $gray-light;
.title {
font-weight: normal;
.todo-avatar,
.todo-item {
opacity: .6;
}
}
.todo-avatar,
.todo-item {
opacity: .2;
}
.btn {
background-color: $gray-light;
}
}
}
.todo-item {
@include transition(opacity);
.todo-title {
display: flex;
......
......@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController
def create
opts = {
force_random_password: true,
password_expires_at: nil
reset_password: true,
skip_confirmation: true
}
@user = User.new(user_params.merge(opts))
@user.created_by_id = current_user.id
@user.generate_password
@user.generate_reset_token
@user.skip_confirmation!
@user = Users::CreateService.new(current_user, user_params.merge(opts)).execute
respond_to do |format|
if @user.save
if @user.persisted?
format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user }
else
......
......@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
params.require(:user).permit(:notification_email)
params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
end
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
layout 'project'
def index
......@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
render json: BuildSerializer
.new(project: @project, user: @current_user)
.represent_status(@build)
end
def erase
......
......@@ -35,8 +35,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def disable
deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id])
return render_404 unless deploy_key_project
deploy_key_project.destroy!
load_key
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
log_audit_event(@key.title, action: :destroy)
redirect_to_repository_settings(@project)
......
......@@ -273,4 +273,13 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
def authenticate_user!
return if current_user
notice = "Please sign in to create the new issue."
store_location_for :user, request.fullpath
redirect_to new_user_session_path, notice: notice
end
end
......@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues,
:ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues,
# EE
:approve, :approvals, :unapprove, :rebase
]
......@@ -100,31 +100,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@environment = @merge_request.environments_for(current_user).last
if @start_sha
compared_diff_version
else
......@@ -428,7 +428,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
@commit = @repository.commit(@ref)
@commit = @repository.commit("refs/heads/#{@ref}")
end
render layout: false
......@@ -439,7 +439,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:ref].present?
@ref = params[:ref]
@commit = @target_project.commit(@ref)
@commit = @target_project.commit("refs/heads/#{@ref}")
end
render layout: false
......@@ -499,6 +499,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
def pipeline_status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
def ci_environments_status
environments =
begin
......
......@@ -13,11 +13,14 @@ class Projects::MilestonesController < Projects::ApplicationController
def index
@milestones =
case params[:state]
when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc)
when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc)
else @project.milestones.active.reorder(due_date: :asc, title: :asc)
when 'all' then @project.milestones
when 'closed' then @project.milestones.closed
else @project.milestones.active
end
@sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort)
@milestones = @milestones.includes(:project)
respond_to do |format|
format.html do
......
......@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent_status(@pipeline)
end
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
......
......@@ -130,6 +130,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
params[:wiki].slice(:title, :content, :format, :message)
params.require(:wiki).permit(:title, :content, :format, :message)
end
end
class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
include Recaptcha::Verify
def new
......@@ -21,6 +20,8 @@ class RegistrationsController < Devise::RegistrationsController
flash.delete :recaptcha_error
render action: 'new'
end
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
end
def destroy
......@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController
private
def signup_enabled?
unless current_application_settings.signup_enabled?
redirect_to(new_user_session_path)
end
end
def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
......@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
@resource ||= User.new(sign_up_params)
@resource ||= Users::CreateService.new(current_user, sign_up_params).build
end
def devise_mapping
......
......@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project?
if project
label_ids << project.group.labels if project.group.present?
label_ids << project.labels
if project.group.present?
labels_table = Label.arel_table
label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
)
)
else
label_ids << project.labels
end
end
else
label_ids << Label.where(group_id: projects.group_ids)
......
......@@ -89,10 +89,12 @@ module MilestonesHelper
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " remaining"
content.html_safe
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
content.html_safe
end
end
......
......@@ -6,7 +6,13 @@ module NamespacesHelper
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
groups << extra_group if extra_group && !Group.exists?(name: extra_group.name)
unless extra_group.nil? || extra_group.is_a?(Group)
extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
end
if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group))
groups |= [extra_group]
end
users = [current_user.namespace]
......
......@@ -31,7 +31,11 @@ module NavHelper
end
def layout_nav_class
"page-with-layout-nav" if defined?(nav) && nav
class_name = ''
class_name << " page-with-layout-nav" if defined?(nav) && nav
class_name << " page-with-sub-nav" if content_for?(:sub_nav)
class_name
end
def nav_control_class
......
......@@ -2,6 +2,7 @@ module SortingHelper
def sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
......@@ -52,6 +53,17 @@ module SortingHelper
}
end
def milestone_sort_options_hash
{
sort_value_name => sort_title_name_asc,
sort_value_name_desc => sort_title_name_desc,
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_start_date_soon => sort_title_start_date_soon,
sort_value_start_date_later => sort_title_start_date_later,
}
end
def sort_title_priority
'Priority'
end
......@@ -92,6 +104,14 @@ module SortingHelper
'Due later'
end
def sort_title_start_date_soon
'Start soon'
end
def sort_title_start_date_later
'Start later'
end
def sort_title_name
'Name'
end
......@@ -212,6 +232,14 @@ module SortingHelper
'due_date_desc'
end
def sort_value_start_date_soon
'start_date_asc'
end
def sort_value_start_date_later
'start_date_desc'
end
def sort_value_name
'name_asc'
end
......
module UsersHelper
def user_link(user)
link_to(user.name, user_path(user),
title: user.email,
class: 'has-tooltip commit-committer-link')
end
end
......@@ -6,8 +6,8 @@ class Board < ActiveRecord::Base
validates :name, :project, presence: true
def done_list
lists.merge(List.done).take
def closed_list
lists.merge(List.closed).take
end
def milestone
......
......@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
enum list_type: { label: 1, done: 2 }
enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
......
......@@ -161,8 +161,10 @@ class MergeRequest < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
# unscoping unnecessary conditions that'll be applied
# when executing `where("merge_requests.id IN (#{union.to_sql})")`
source = unscoped.where(source_project_id: relation).select(:id)
target = unscoped.where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
......
......@@ -109,6 +109,21 @@ class Milestone < ActiveRecord::Base
end
end
def self.sort(method)
case method.to_s
when 'due_date_asc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
when 'due_date_desc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
when 'start_date_asc'
reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC'))
when 'start_date_desc'
reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC'))
else
order_by(method)
end
end
##
# Returns the String necessary to reference this Milestone in Markdown
#
......
......@@ -356,20 +356,15 @@ class Project < ActiveRecord::Base
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
# unscoping unnecessary conditions that'll be applied
# when executing `where("projects.id IN (#{union.to_sql})")`
projects = unscoped.select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
# We explicitly remove any eager loading clauses as they're:
#
# 1. Not needed by this query
# 2. Combined with .joins(:namespace) lead to all columns from the
# projects & namespaces tables being selected, leading to a SQL error
# due to the columns of all UNION'd queries no longer being the same.
namespaces = select(:id).
except(:includes).
namespaces = unscoped.select(:id).
joins(:namespace).
where(ntable[:name].matches(pattern))
......
......@@ -1024,7 +1024,7 @@ class Repository
end
def diverged_from_upstream?(branch_name)
branch_commit = commit(branch_name)
branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit
......@@ -1035,7 +1035,7 @@ class Repository
end
def upstream_has_diverged?(branch_name, remote_ref)
branch_commit = commit(branch_name)
branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{remote_ref}/#{branch_name}")
if upstream_commit
......@@ -1046,7 +1046,7 @@ class Repository
end
def up_to_date_with_upstream?(branch_name)
branch_commit = commit(branch_name)
branch_commit = commit("refs/heads/#{branch_name}")
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit
......
......@@ -24,6 +24,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
......@@ -126,7 +127,9 @@ class User < ActiveRecord::Base
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
presence: true,
......@@ -137,10 +140,9 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
......@@ -150,8 +152,6 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
before_create :check_confirmation_email
after_create :post_create_hook
after_destroy :post_destroy_hook
# User's Layout preference
......@@ -409,10 +409,8 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}"
end
def generate_password
if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
def skip_confirmation=(bool)
skip_confirmation! if bool
end
def generate_reset_token
......@@ -424,10 +422,6 @@ class User < ActiveRecord::Base
@reset_token
end
def check_confirmation_email
skip_confirmation! unless current_application_settings.send_user_confirmation_email
end
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
......@@ -822,12 +816,6 @@ class User < ActiveRecord::Base
end
end
def post_create_hook
log_info("User \"#{name}\" (#{email}) was created")
notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
......
......@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
private
alias_method :build, :object
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
def detailed_status
build.detailed_status(request.user)
end
end
class BuildSerializer < BaseSerializer
entity BuildEntity
def represent_status(resource)
data = represent(resource, { only: [:status] })
data.fetch(:status, {})
end
end
......@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
expose :status do |pipeline, options|
StatusEntity.represent(
pipeline.detailed_status(request.user),
options)
end
expose :detailed_status, as: :status, with: StatusEntity
expose :duration
expose :finished_at
expose :stages, using: StageEntity
......@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
def detailed_status
pipeline.detailed_status(request.user)
end
end
......@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
end
def represent_status(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
end
end
class StatusEntity < Grape::Entity
include RequestAwareEntity
expose :icon, :text, :label, :group
expose :icon, :favicon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
......
......@@ -4,7 +4,7 @@ module Boards
board = project.boards.create(params)
if board.persisted?
board.lists.create(list_type: :done)
board.lists.create(list_type: :closed)
end
board
......
......@@ -41,7 +41,7 @@ module Boards
end
def set_state
params[:state] = list && list.done? ? 'closed' : 'opened'
params[:state] = list && list.closed? ? 'closed' : 'opened'
end
def board_label_ids
......
......@@ -48,8 +48,8 @@ module Boards
end
def issue_state
return 'reopen' if moving_from_list.done?
return 'close' if moving_to_list.done?
return 'reopen' if moving_from_list.closed?
return 'close' if moving_to_list.closed?
end
def add_label_ids
......
......@@ -7,14 +7,14 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
pipeline.builds.failed_or_canceled.find_each do |build|
pipeline.builds.latest.failed_or_canceled.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user)
.reprocess(build)
end
pipeline.builds.skipped.find_each do |skipped|
pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
......
......@@ -38,7 +38,7 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) if skip_current_user
recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
recipients.uniq
end
......@@ -47,7 +47,7 @@ class NotificationRecipientService
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
recipients.delete(current_user) unless current_user.notified_of_own_activity?
recipients.uniq
end
......@@ -88,7 +88,7 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
recipients.delete(note.author)
recipients.delete(note.author) unless note.author.notified_of_own_activity?
recipients.uniq
end
......
......@@ -297,8 +297,9 @@ class NotificationService
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline,
nil, # The acting user, who won't be added to recipients
action: pipeline.status).map(&:notification_email)
pipeline.user,
action: pipeline.status,
skip_current_user: false).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
......
module Users
# Service for creating a new user.
class CreateService < BaseService
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
end
def build
raise Gitlab::Access::AccessDeniedError unless can_create_user?
user = User.new(build_user_params)
if current_user&.is_admin?
if params[:reset_password]
@reset_token = user.generate_reset_token
params[:force_random_password] = true
end
if params[:force_random_password]
random_password = Devise.friendly_token.first(Devise.password_length.min)
user.password = user.password_confirmation = random_password
end
end
identity_attrs = params.slice(:extern_uid, :provider)
if identity_attrs.any?
user.identities.build(identity_attrs)
end
user
end
def execute
user = build
if user.save
log_info("User \"#{user.name}\" (#{user.email}) was created")
notification_service.new_user(user, @reset_token) if @reset_token
system_hook_service.execute_hooks_for(user, :create)
end
user
end
private
def can_create_user?
(current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin?
end
# Allowed params for creating a user (admins only)
def admin_create_params
[
:access_level,
:admin,
:avatar,
:bio,
:can_create_group,
:color_scheme_id,
:email,
:external,
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
:linkedin,
:name,
:password,
:password_expires_at,
:projects_limit,
:remember_me,
:skip_confirmation,
:skype,
:theme_id,
:twitter,
:username,
:website_url
]
end
# Allowed params for user signup
def signup_params
[
:email,
:email_confirmation,
:name,
:password,
:username
]
end
def build_user_params
if current_user&.is_admin?
user_params = params.slice(*admin_create_params)
user_params[:created_by_id] = current_user.id
if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil)
end
else
user_params = params.slice(*signup_params)
user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
end
user_params
end
end
end
......@@ -2,7 +2,7 @@
%legend Access
.form-group
= f.label :projects_limit, class: 'control-label'
.col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group
= f.label :can_create_group, class: 'control-label'
......
......@@ -68,12 +68,11 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
.js-todos-all
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
.panel.panel-default.panel-small.panel-without-border
.panel.panel-default.panel-without-border.panel-without-margin
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
......
......@@ -8,7 +8,7 @@
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{ for: "user_remember_me" }
= f.check_box :remember_me
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
......
......@@ -17,24 +17,3 @@
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
- if @tags.present?
.dropdown
%button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('tags')
%span.light Tags:
- if params[:tag].present?
= params[:tag]
- else
Any
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
Any
- @tags.each do |tag|
%li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do
= icon('tag')
= tag.name
......@@ -3,8 +3,9 @@
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
- if content_for?(:sub_nav)
= yield :sub_nav
.content-wrapper{ class: layout_nav_class }
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/flash"
......
......@@ -15,6 +15,13 @@
%span.sr-only Toggle navigation
= icon('ellipsis-v')
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
.title-container
%h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse
%ul.nav.navbar-nav
%li.hidden-sm.hidden-xs
......@@ -69,12 +76,6 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
.header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
%h1.title{ class: ('initializing' if @has_group_title) }= title
= yield :header_content
= render 'shared/outdated_browser'
......
......@@ -34,6 +34,11 @@
.clearfix
= form_for @user, url: profile_notifications_path, method: :put do |f|
%label{ for: 'user_notified_of_own_activity' }
= f.check_box :notified_of_own_activity
%span Receive notifications about your own activity
%hr
%h5
Groups (#{@group_notifications.count})
......
......@@ -7,12 +7,12 @@
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "done"',
"v-if" => 'list.type !== "closed"',
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
......
......@@ -3,7 +3,7 @@
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "done" && showIssueForm' }
"v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
......
- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Job
%strong ##{@build.id}
......@@ -137,3 +137,6 @@
= build.id
- if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
:javascript
new Sidebar();
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs"
= render "projects/pipelines/head", build_subnav: true
= render "projects/pipelines/head"
%div{ class: container_class }
.build-page
......
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
} }
- content_for :page_specific_javascripts do
......
......@@ -16,7 +16,7 @@
.col-sm-6
.nav-controls
= link_to @environment.external_url, class: 'btn btn-default' do
= link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do
= icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
......
- case @status
- when :success
- remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch?
:plain
merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
merge_request_widget.mergeInProgress(#{remove_source_branch});
- when :merge_when_pipeline_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
......
......@@ -7,6 +7,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do
New Milestone
......
......@@ -23,7 +23,7 @@
- if current_user.can_select_namespace?
.input-group-addon
= root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
- else
.input-group-addon.static-namespace
......
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) }
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- if project_nav_tab? :pipelines
= nav_link(path: 'pipelines#index', controller: :pipelines) do
......@@ -10,13 +10,13 @@
Pipelines
- if project_nav_tab? :builds
= nav_link(path: 'builds#index', controller: :builds) do
= nav_link(controller: :builds) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
- if project_nav_tab? :environments
= nav_link(path: 'environments#index', controller: :environments) do
= nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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