Commit 7788118c authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge remote-tracking branch 'origin/master' into add-sentry-js-again-with-vue

parents 637ed8a2 cac04fbd
...@@ -45,6 +45,7 @@ eslint-report.html ...@@ -45,6 +45,7 @@ eslint-report.html
/public/uploads.* /public/uploads.*
/public/uploads/ /public/uploads/
/shared/artifacts/ /shared/artifacts/
/spec/javascripts/fixtures/blob/pdf/
/rails_best_practices_output.html /rails_best_practices_output.html
/tags /tags
/tmp/* /tmp/*
......
...@@ -19,8 +19,8 @@ variables: ...@@ -19,8 +19,8 @@ variables:
before_script: before_script:
- bundle --version - bundle --version
- . scripts/utils.sh - source scripts/utils.sh
- ./scripts/prepare_build.sh - source scripts/prepare_build.sh
stages: stages:
- prepare - prepare
...@@ -253,38 +253,46 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -253,38 +253,46 @@ spinach mysql 9 10: *spinach-knapsack-mysql
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
.exec: &exec .rake-exec: &rake-exec
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
stage: test stage: test
script: script:
- bundle exec $CI_JOB_NAME - bundle exec rake $CI_JOB_NAME
rubocop: static-analysis:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- bundle exec "rubocop --require rubocop-rspec" - scripts/static-analysis
rake haml_lint: *exec docs:check:links:
rake scss_lint: *exec image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
rake config_lint: *exec stage: test
rake brakeman: *exec <<: *dedicated-runner
rake flay: *exec cache: {}
license_finder: *exec dependencies: []
rake downtime_check: before_script: []
<<: *exec script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
downtime_check:
<<: *rake-exec
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?$/
- /^docs\/*/ - /^docs\/*/
rake ee_compat_check: ee_compat_check:
<<: *exec <<: *rake-exec
only: only:
- branches@gitlab-org/gitlab-ce - branches@gitlab-org/gitlab-ce
except: except:
...@@ -306,22 +314,22 @@ rake ee_compat_check: ...@@ -306,22 +314,22 @@ rake ee_compat_check:
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
rake pg db:migrate:reset: rake pg db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:migrate:reset: rake mysql db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback .db-rollback: &db-rollback
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:rollback STEP=120 - bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate - bundle exec rake db:migrate
...@@ -329,16 +337,15 @@ rake mysql db:migrate:reset: ...@@ -329,16 +337,15 @@ rake mysql db:migrate:reset:
rake pg db:rollback: rake pg db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:rollback: rake mysql db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
...@@ -356,12 +363,10 @@ rake mysql db:rollback: ...@@ -356,12 +363,10 @@ rake mysql db:rollback:
rake pg db:seed_fu: rake pg db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:seed_fu: rake mysql db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
<<: *except-docs
rake gitlab:assets:compile: rake gitlab:assets:compile:
stage: test stage: test
...@@ -402,31 +407,6 @@ rake karma: ...@@ -402,31 +407,6 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
docs:check:apilint:
image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
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:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -485,14 +465,6 @@ coverage: ...@@ -485,14 +465,6 @@ coverage:
- coverage/index.html - coverage/index.html
- coverage/assets/ - coverage/assets/
lint:javascript:
<<: *dedicated-runner
<<: *except-docs
stage: test
before_script: []
script:
- yarn run eslint
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
...@@ -526,22 +498,6 @@ trigger_docs: ...@@ -526,22 +498,6 @@ trigger_docs:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
stage: post-test
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
pages: pages:
before_script: [] before_script: []
stage: pages stage: pages
......
...@@ -961,7 +961,7 @@ RSpec/DescribeSymbol: ...@@ -961,7 +961,7 @@ RSpec/DescribeSymbol:
# Checks that the second argument to top level describe is the tested method # Checks that the second argument to top level describe is the tested method
# name. # name.
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: false Enabled: true
# Checks for long example. # Checks for long example.
RSpec/ExampleLength: RSpec/ExampleLength:
...@@ -983,10 +983,12 @@ RSpec/ExpectActual: ...@@ -983,10 +983,12 @@ RSpec/ExpectActual:
# Checks the file and folder naming of the spec file. # Checks the file and folder naming of the spec file.
RSpec/FilePath: RSpec/FilePath:
Enabled: false Enabled: true
CustomTransform: IgnoreMethods: true
RuboCop: rubocop Exclude:
RSpec: rspec - 'qa/**/*'
- 'spec/javascripts/fixtures/*'
- 'spec/requests/api/v3/*'
# Checks if there are focused specs. # Checks if there are focused specs.
RSpec/Focus: RSpec/Focus:
......
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
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.1.2 (2017-05-01)
- Add index on ci_runners.contacted_at. !10876 (blackst0ne)
- Fix pipeline events description for Slack and Mattermost integration. !10908
- Fixed milestone sidebar showing incorrect number of MRs when collapsed. !10933
- Fix ordering of commits in the network graph. !10936
- Ensure the chat notifications service properly saves the "Notify only default branch" setting. !10959
- Lazily sets UUID in ApplicationSetting for new installations.
- Skip validation when creating internal (ghost, service desk) users.
- Use GitLab Pages v0.4.1.
## 9.1.1 (2017-04-26) ## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465 - Add a transaction around move_issues_to_ghost_user. !10465
......
...@@ -85,14 +85,14 @@ gem 'kaminari', '~> 0.17.0' ...@@ -85,14 +85,14 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
# Files attachments # Files attachments
gem 'carrierwave', '~> 0.11.0' gem 'carrierwave', '~> 1.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5' gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
......
...@@ -105,12 +105,10 @@ GEM ...@@ -105,12 +105,10 @@ GEM
capybara-screenshot (1.0.14) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (0.11.2) carrierwave (1.0.0)
activemodel (>= 3.2.0) activemodel (>= 4.0.0)
activesupport (>= 3.2.0) activesupport (>= 4.0.0)
json (>= 1.7)
mime-types (>= 1.16) mime-types (>= 1.16)
mimemagic (>= 0.3.0)
cause (0.1) cause (0.1)
charlock_holmes (0.7.3) charlock_holmes (0.7.3)
chronic (0.10.2) chronic (0.10.2)
...@@ -184,7 +182,7 @@ GEM ...@@ -184,7 +182,7 @@ GEM
erubis (2.7.0) erubis (2.7.0)
escape_utils (1.1.1) escape_utils (1.1.1)
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.52.0) excon (0.55.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extlib (0.9.16) extlib (0.9.16)
...@@ -210,12 +208,12 @@ GEM ...@@ -210,12 +208,12 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog-aws (0.11.0) fog-aws (0.13.0)
fog-core (~> 1.38) fog-core (~> 1.38)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-core (1.42.0) fog-core (1.44.1)
builder builder
excon (~> 0.49) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
...@@ -237,9 +235,9 @@ GEM ...@@ -237,9 +235,9 @@ GEM
fog-json (>= 1.0) fog-json (>= 1.0)
fog-xml (>= 0.1) fog-xml (>= 0.1)
ipaddress (>= 0.8) ipaddress (>= 0.8)
fog-xml (0.1.2) fog-xml (0.1.3)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1) font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.78.0)
...@@ -330,7 +328,7 @@ GEM ...@@ -330,7 +328,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.1.2) grpc (1.2.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
...@@ -871,7 +869,7 @@ DEPENDENCIES ...@@ -871,7 +869,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.6.2) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.11.0) carrierwave (~> 1.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
...@@ -896,7 +894,7 @@ DEPENDENCIES ...@@ -896,7 +894,7 @@ DEPENDENCIES
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.8.0) flay (~> 2.8.0)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.40) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
......
app/assets/images/ci_favicons/favicon_status_canceled.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_created.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_failed.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_manual.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_not_found.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_pending.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_running.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_skipped.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_success.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_warning.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
  • 2-up
  • Swipe
  • Onion skin
...@@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) { ...@@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) {
data-fallback-src="${fallbackImageSrc}" data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute} ${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}" data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
> >
${contents} ${contents}
</gl-emoji> </gl-emoji>
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import Vue from 'vue'; import Vue from 'vue';
import PDFLab from 'vendor/pdflab'; import pdfLab from '../../pdf/index.vue';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => { export default () => {
const el = document.getElementById('js-pdf-viewer'); const el = document.getElementById('js-pdf-viewer');
...@@ -20,6 +15,9 @@ export default () => { ...@@ -20,6 +15,9 @@ export default () => {
pdf: el.dataset.endpoint, pdf: el.dataset.endpoint,
}; };
}, },
components: {
pdfLab,
},
methods: { methods: {
onLoad() { onLoad() {
this.loading = false; this.loading = false;
......
...@@ -6,7 +6,7 @@ export default class BlobViewer { ...@@ -6,7 +6,7 @@ export default class BlobViewer {
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]'); this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]'); this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$blobContentHolder = $('#blob-content-holder'); this.$fileHolder = $('.file-holder');
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type'); let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
...@@ -82,7 +82,7 @@ export default class BlobViewer { ...@@ -82,7 +82,7 @@ export default class BlobViewer {
viewer.setAttribute('data-loaded', 'true'); viewer.setAttribute('data-loaded', 'true');
this.$blobContentHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState(); this.toggleCopyButtonState();
}); });
......
...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
computed: { computed: {
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
} }
}, },
watch: { watch: {
......
...@@ -44,6 +44,7 @@ import GroupsList from './groups_list'; ...@@ -44,6 +44,7 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
...@@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ProjectsList(); new ProjectsList();
break; break;
case 'dashboard:groups:index': case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index': case 'explore:groups:index':
new GroupsList(); new GroupsList();
const landingElement = document.querySelector('.js-explore-groups-landing');
if (!landingElement) break;
const exploreGroupsLanding = new Landing(
landingElement,
landingElement.querySelector('.dismiss-button'),
'explore_groups_landing_dismissed',
);
exploreGroupsLanding.toggle();
break; break;
case 'projects:milestones:new': case 'projects:milestones:new':
case 'projects:milestones:edit': case 'projects:milestones:edit':
...@@ -356,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -356,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'users:show': case 'users:show':
new UserCallout(); new UserCallout();
break; break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'sessions': case 'sessions':
...@@ -434,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -434,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
if (path[2] === 'show') { if (path[2] === 'show') {
new ZenMode(); new ZenMode();
new LineHighlighter();
new BlobViewer();
} }
break; break;
case 'labels': case 'labels':
......
import Cookies from 'js-cookie';
class Landing {
constructor(landingElement, dismissButton, cookieName) {
this.landingElement = landingElement;
this.cookieName = cookieName;
this.dismissButton = dismissButton;
this.eventWrapper = {};
}
toggle() {
const isDismissed = this.isDismissed();
this.landingElement.classList.toggle('hidden', isDismissed);
if (!isDismissed) this.addEvents();
}
addEvents() {
this.eventWrapper.dismissLanding = this.dismissLanding.bind(this);
this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding);
}
removeEvents() {
this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding);
}
dismissLanding() {
this.landingElement.classList.add('hidden');
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
isDismissed() {
return Cookies.get(this.cookieName) === 'true';
}
}
export default Landing;
...@@ -57,9 +57,9 @@ require('vendor/jquery.scrollTo'); ...@@ -57,9 +57,9 @@ require('vendor/jquery.scrollTo');
} }
LineHighlighter.prototype.bindEvents = function() { LineHighlighter.prototype.bindEvents = function() {
const $blobContentHolder = $('#blob-content-holder'); const $fileHolder = $('.file-holder');
$blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler); $fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$blobContentHolder.on('highlight:line', this.highlightHash); $fileHolder.on('highlight:line', this.highlightHash);
}; };
LineHighlighter.prototype.highlightHash = function() { LineHighlighter.prototype.highlightHash = function() {
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
import d3 from 'd3'; import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash'; import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container'; const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state'; const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
...@@ -36,6 +40,7 @@ class PrometheusGraph { ...@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right; this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} else { } else {
...@@ -74,6 +79,12 @@ class PrometheusGraph { ...@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show(); $(prometheusParentGraphContainer).show();
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
} }
}); });
} }
...@@ -96,6 +107,7 @@ class PrometheusGraph { ...@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top) .attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g') .append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`); .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer) const axisLabelContainer = d3.select(prometheusGraphContainer)
...@@ -116,6 +128,7 @@ class PrometheusGraph { ...@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y) .scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks) .ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width) .tickSize(-this.width)
.outerTickSize(0)
.orient('left'); .orient('left');
this.createAxisLabelContainers(axisLabelContainer, key); this.createAxisLabelContainers(axisLabelContainer, key);
...@@ -248,7 +261,8 @@ class PrometheusGraph { ...@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex]; const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0; const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time); const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData); const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
...@@ -256,13 +270,12 @@ class PrometheusGraph { ...@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag // Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line') currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({ .attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate, x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0), y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate, x2: currentTimeCoordinate,
...@@ -272,33 +285,45 @@ class PrometheusGraph { ...@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle') currentChart.append('circle')
.attr('class', 'circle-metric') .attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color) .attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate) .attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value)) .attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric); .attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text // The little box with text
const rectTextMetric = currentChart.append('g') const rectTextMetric = currentChart.append('svg')
.attr('class', 'rect-text-metric') .attr({
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect') rectTextMetric.append('rect')
.attr('class', 'rect-metric') .attr({
.attr('x', currentTimeCoordinate + 10) class: 'rect-metric',
.attr('y', maxMetricValue) x: 4,
.attr('width', this.commonGraphProperties.rect_text_width) y: 1,
.attr('height', this.commonGraphProperties.rect_text_height); rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric') .attr({
.attr('x', currentTimeCoordinate + 35) class: 'text-metric text-metric-bold',
.attr('y', maxMetricValue + 35) x: 8,
y: 35,
})
.text(timeFormat(currentData.time)); .text(timeFormat(currentData.time));
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric-date') .attr({
.attr('x', currentTimeCoordinate + 15) class: 'text-metric-date',
.attr('y', maxMetricValue + 15) x: 8,
.text(dayFormat(currentData.time)); y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value); let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') { if (key === 'cpu_values') {
......
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<script>
import pdfjsLib from 'pdfjs-dist';
import workerSrc from 'vendor/pdf.worker';
import page from './page/index.vue';
export default {
props: {
pdf: {
type: [String, Uint8Array],
required: true,
},
},
data() {
return {
loading: false,
pages: [],
};
},
components: { page },
watch: { pdf: 'load' },
computed: {
document() {
return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf };
},
hasPDF() {
return this.pdf && this.pdf.length > 0;
},
},
methods: {
load() {
this.pages = [];
return pdfjsLib.getDocument(this.document)
.then(this.renderPages)
.then(() => this.$emit('pdflabload'))
.catch(error => this.$emit('pdflaberror', error))
.then(() => { this.loading = false; });
},
renderPages(pdf) {
const pagePromises = [];
this.loading = true;
for (let num = 1; num <= pdf.numPages; num += 1) {
pagePromises.push(
pdf.getPage(num).then(p => this.pages.push(p)),
);
}
return Promise.all(pagePromises);
},
},
mounted() {
pdfjsLib.PDFJS.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
};
</script>
<style>
.pdf-viewer {
background: url('./assets/img/bg.gif');
display: flex;
flex-flow: column nowrap;
}
</style>
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<script>
export default {
props: {
page: {
type: Object,
required: true,
},
number: {
type: Number,
required: true,
},
},
data() {
return {
scale: 4,
rendering: false,
};
},
computed: {
viewport() {
return this.page.getViewport(this.scale);
},
context() {
return this.$refs.canvas.getContext('2d');
},
renderContext() {
return {
canvasContext: this.context,
viewport: this.viewport,
};
},
},
mounted() {
this.$refs.canvas.height = this.viewport.height;
this.$refs.canvas.width = this.viewport.width;
this.rendering = true;
this.page.render(this.renderContext)
.then(() => { this.rendering = false; })
.catch(error => this.$emit('pdflaberror', error));
},
};
</script>
<style>
.pdf-page {
margin: 8px auto 0 auto;
border-top: 1px #ddd solid;
border-bottom: 1px #ddd solid;
width: 100%;
}
.pdf-page:first-child {
margin-top: 0px;
border-top: 0px;
}
.pdf-page:last-child {
margin-bottom: 0px;
border-bottom: 0px;
}
</style>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
$els.each((function(_this) { $els.each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id'); options.groupId = $dropdown.data('group-id');
...@@ -38,11 +38,11 @@ ...@@ -38,11 +38,11 @@
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
selectedId = $dropdown.data('selected');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
issueURL = $dropdown.data('issueUpdate'); issueURL = $dropdown.data('issueUpdate');
$selectbox = $dropdown.closest('.selectbox'); $selectbox = $dropdown.closest('.selectbox');
...@@ -51,6 +51,8 @@ ...@@ -51,6 +51,8 @@
$value = $block.find('.value'); $value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault;
var updateIssueBoardsIssue = function () { var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
...@@ -186,12 +188,14 @@ ...@@ -186,12 +188,14 @@
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
$dropdown.find('.dropdown-toggle-text').removeClass('is-default');
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
return selected.name; return selected.name;
} }
} else { } else {
$dropdown.find('.dropdown-toggle-text').addClass('is-default');
return defaultLabel; return defaultLabel;
} }
}, },
...@@ -204,13 +208,14 @@ ...@@ -204,13 +208,14 @@
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) { clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected, isSelecting;
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault(); e.preventDefault();
selectedId = user.id;
if (selectedId === gon.current_user_id) { if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide(); $('.assign-to-me-link').hide();
} else { } else {
...@@ -221,12 +226,11 @@ ...@@ -221,12 +226,11 @@
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) { if (user.id && isSelecting) {
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id, id: user.id,
username: user.username, username: user.username,
...@@ -248,6 +252,9 @@ ...@@ -248,6 +252,9 @@
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
}
$el.find('.is-active').removeClass('is-active'); $el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
}, },
......
...@@ -227,8 +227,8 @@ ...@@ -227,8 +227,8 @@
.award-control-icon-positive, .award-control-icon-positive,
.award-control-icon-super-positive { .award-control-icon-super-positive {
position: absolute; position: absolute;
left: 7px; left: 11px;
bottom: 9px; bottom: 7px;
opacity: 0; opacity: 0;
@include transition(opacity, transform); @include transition(opacity, transform);
} }
......
...@@ -254,6 +254,63 @@ ...@@ -254,6 +254,63 @@
padding: 10px 0; padding: 10px 0;
} }
.landing {
margin-bottom: $gl-padding;
overflow: hidden;
display: flex;
position: relative;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
background-color: $blue-25;
justify-content: center;
.dismiss-button {
position: absolute;
right: 6px;
top: 6px;
cursor: pointer;
color: $blue-300;
z-index: 1;
border: none;
background-color: transparent;
&:hover,
&:focus {
border: none;
color: $blue-400;
}
}
.svg-container {
align-self: center;
}
.inner-content {
text-align: left;
white-space: nowrap;
h4 {
color: $gl-text-color;
font-size: 17px;
}
p {
color: $gl-text-color;
margin-bottom: $gl-padding;
}
}
@media (max-width: $screen-sm-min) {
flex-direction: column;
.inner-content {
white-space: normal;
padding: 0 28px;
text-align: center;
}
}
}
.empty-state { .empty-state {
margin: 100px 0 0; margin: 100px 0 0;
......
...@@ -424,6 +424,11 @@ table { ...@@ -424,6 +424,11 @@ table {
} }
} }
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.str-truncated { .str-truncated {
&-60 { &-60 {
@include str-truncated(60%); @include str-truncated(60%);
......
...@@ -390,7 +390,8 @@ ...@@ -390,7 +390,8 @@
&::before { &::before {
position: absolute; position: absolute;
left: 6px; left: 6px;
top: 6px; top: 50%;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
......
...@@ -61,11 +61,13 @@ ...@@ -61,11 +61,13 @@
.file-content { .file-content {
background: $white-light; background: $white-light;
&.image_file { &.image_file,
&.video {
background: $file-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
img { img,
video {
padding: 20px; padding: 20px;
max-width: 80%; max-width: 80%;
} }
......
...@@ -93,11 +93,6 @@ ...@@ -93,11 +93,6 @@
top: $gl-padding-top; top: $gl-padding-top;
} }
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.content-list { .content-list {
li { li {
padding: 18px $gl-padding $gl-padding; padding: 18px $gl-padding $gl-padding;
...@@ -139,42 +134,9 @@ ...@@ -139,42 +134,9 @@
} }
} }
.landing { .landing svg {
margin-bottom: $gl-padding; width: 136px;
overflow: hidden; height: 136px;
.dismiss-icon {
position: absolute;
right: $cycle-analytics-box-padding;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
text-align: center;
svg {
width: 136px;
height: 136px;
}
}
.inner-content {
@media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
h4 {
color: $gl-text-color;
font-size: 17px;
}
p {
color: $cycle-analytics-box-text-color;
margin-bottom: $gl-padding;
}
}
} }
.fa-spinner { .fa-spinner {
......
...@@ -157,7 +157,8 @@ ...@@ -157,7 +157,8 @@
.prometheus-graph { .prometheus-graph {
text { text {
fill: $stat-graph-axis-fill; fill: $gl-text-color;
stroke-width: 0;
} }
.label-axis-text, .label-axis-text,
...@@ -210,27 +211,33 @@ ...@@ -210,27 +211,33 @@
.rect-text-metric { .rect-text-metric {
fill: $white-light; fill: $white-light;
stroke-width: 1; stroke-width: 1;
stroke: $black; stroke: $gray-darkest;
} }
.rect-axis-text { .rect-axis-text {
fill: $white-light; fill: $white-light;
} }
.text-metric, .text-metric {
.text-median-metric, font-weight: 600;
.text-metric-usage,
.text-metric-date {
fill: $black;
} }
.text-metric-date { .selected-metric-line {
font-weight: 200; stroke: $gl-gray-dark;
stroke-width: 1;
} }
.selected-metric-line { .deployment-line {
stroke: $black; stroke: $black;
stroke-width: 1; stroke-width: 2;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
} }
.prometheus-state { .prometheus-state {
......
...@@ -88,3 +88,26 @@ ...@@ -88,3 +88,26 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
margin-top: 10px; margin-top: 10px;
} }
.explore-groups.landing {
margin-top: 10px;
.inner-content {
padding: 0;
p {
margin: 7px 0 0;
max-width: 480px;
padding: 0 $gl-padding;
@media (max-width: $screen-sm-min) {
margin: 0 auto;
}
}
}
svg {
width: 62px;
height: 50px;
}
}
class Admin::HooksController < Admin::ApplicationController class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit
def index def index
@hooks = SystemHook.all @hooks = SystemHook.all
@hook = SystemHook.new @hook = SystemHook.new
...@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end end
end end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
render 'edit'
end
end
def destroy def destroy
@hook = SystemHook.find(params[:id]) hook.destroy
@hook.destroy
redirect_to admin_hooks_path redirect_to admin_hooks_path
end end
def test def test
@hook = SystemHook.find(params[:hook_id])
data = { data = {
event_name: "project_create", event_name: "project_create",
name: "Ruby", name: "Ruby",
...@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone", owner_name: "Someone",
owner_email: "example@gitlabhq.com" owner_email: "example@gitlabhq.com"
} }
@hook.execute(data, 'system_hooks') hook.execute(data, 'system_hooks')
redirect_back_or_default redirect_back_or_default
end end
private
def hook
@hook ||= SystemHook.find(params[:id])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:enable_ssl_verification, :enable_ssl_verification,
......
module NotesActions
include RendersNotes
extend ActiveSupport::Concern
included do
before_action :authorize_admin_note!, only: [:update, :destroy]
end
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end
private
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
end
...@@ -14,4 +14,8 @@ module RendersBlob ...@@ -14,4 +14,8 @@ module RendersBlob
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false) html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
} }
end end
def override_max_blob_size(blob)
blob.override_max_size! if params[:override_max_size] == 'true'
end
end end
...@@ -10,6 +10,8 @@ module RendersNotes ...@@ -10,6 +10,8 @@ module RendersNotes
private private
def preload_max_access_for_authors(notes, project) def preload_max_access_for_authors(notes, project)
return nil unless project
user_ids = notes.map(&:author_id) user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids) project.team.max_member_access_for_user_ids(user_ids)
end end
......
...@@ -5,10 +5,12 @@ module SnippetsActions ...@@ -5,10 +5,12 @@ module SnippetsActions
end end
def raw def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
send_data( send_data(
convert_line_endings(@snippet.content), convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8', type: 'text/plain; charset=utf-8',
disposition: 'inline', disposition: disposition,
filename: @snippet.sanitized_file_name filename: @snippet.sanitized_file_name
) )
end end
......
...@@ -22,7 +22,8 @@ module ToggleAwardEmoji ...@@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable) def to_todoable(awardable)
case awardable case awardable
when Note when Note
awardable.noteable # we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue when MergeRequest, Issue
awardable awardable
when Snippet when Snippet
......
...@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
end end
def show def show
@blob.override_max_size! if params[:override_max_size] == 'true' override_max_blob_size(@blob)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
.represent_concise(deployments) }
end
private
def environment
@environment ||= project.environments.find(params[:environment_id])
end
end
class Projects::HooksController < Projects::ApplicationController class Projects::HooksController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :hook, only: :edit
respond_to :html respond_to :html
...@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
else
render 'edit'
end
end
def test def test
if !@project.empty_repo? if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include RendersNotes include NotesActions
include ToggleAwardEmoji include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve] before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
def index #
current_fetched_at = Time.now.to_i # This is a fix to make spinach feature tests passing:
# Controller actions are returned from AbstractController::Base and methods of parent classes are
notes_json = { notes: [], last_fetched_at: current_fetched_at } # excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
@notes = notes_finder.execute.inc_relations_for_view # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
@notes = prepare_notes_for_rendering(@notes) #
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
@notes.each do |note| #
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create def create
create_params = note_params.merge( super
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end end
def delete_attachment def delete_attachment
...@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note) def note_html(note)
render_to_string( render_to_string(
"projects/notes/_note", "shared/notes/_note",
layout: false, layout: false,
formats: [:html], formats: [:html],
locals: { note: note } locals: { note: note }
...@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
) )
end end
def note_json(note) def finder_params
attrs = { params.merge(last_fetched_at: last_fetched_at)
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end end
def authorize_resolve_note! def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note) return access_denied! unless can?(current_user, :resolve_note, note)
end end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
end end
...@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -3,6 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include RendersBlob
before_action :module_enabled before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
...@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -55,11 +56,23 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def show def show
@note = @project.notes.new(noteable: @snippet) blob = @snippet.blob
@noteable = @snippet override_max_blob_size(blob)
@discussions = @snippet.discussions respond_to do |format|
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) format.html do
@note = @project.notes.new(noteable: @snippet)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end end
def destroy def destroy
......
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
before_action :snippet
before_action :authorize_read_snippet!, only: [:show, :index, :create]
private
def note
@note ||= snippet.notes.find(params[:id])
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
def note_params
super.merge(noteable_id: params[:snippet_id])
end
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
end
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview include MarkdownPreview
include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet # Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw, :download] before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -15,7 +17,7 @@ class SnippetsController < ApplicationController ...@@ -15,7 +17,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet # Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy] before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets' layout 'snippets'
respond_to :html respond_to :html
...@@ -60,6 +62,23 @@ class SnippetsController < ApplicationController ...@@ -60,6 +62,23 @@ class SnippetsController < ApplicationController
end end
def show def show
blob = @snippet.blob
override_max_blob_size(blob)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format|
format.html do
render 'show'
end
format.json do
render_blob_json(blob)
end
end
end end
def destroy def destroy
...@@ -70,14 +89,6 @@ class SnippetsController < ApplicationController ...@@ -70,14 +89,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path redirect_to snippets_path
end end
def download
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true) render_markdown_preview(params[:text], skip_project_check: true)
end end
......
...@@ -68,6 +68,8 @@ class NotesFinder ...@@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet" when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
when "personal_snippet"
PersonalSnippet.all
else else
raise 'invalid target_type' raise 'invalid target_type'
end end
......
module AwardEmojiHelper module AwardEmojiHelper
def toggle_award_url(awardable) def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note) if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) if awardable.for_personal_snippet?
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
else
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
end
else else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end end
......
...@@ -119,7 +119,15 @@ module BlobHelper ...@@ -119,7 +119,15 @@ module BlobHelper
end end
def blob_raw_url def blob_raw_url
namespace_project_raw_path(@project.namespace, @project, @id) if @snippet
if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
else
raw_snippet_path(@snippet)
end
elsif @blob
namespace_project_raw_path(@project.namespace, @project, @id)
end
end end
# SVGs can contain malicious JavaScript; only include whitelisted # SVGs can contain malicious JavaScript; only include whitelisted
...@@ -209,11 +217,21 @@ module BlobHelper ...@@ -209,11 +217,21 @@ module BlobHelper
end end
def copy_blob_source_button(blob) def copy_blob_source_button(blob)
return unless blob.rendered_as_text?(ignore_errors: false)
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard") clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
end end
def open_raw_file_button(path) def open_raw_blob_button(blob)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } if blob.raw_binary?
icon = icon('download')
title = 'Download'
else
icon = icon('file-code-o')
title = 'Open raw'
end
link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end end
def blob_render_error_reason(viewer) def blob_render_error_reason(viewer)
......
...@@ -74,7 +74,7 @@ module MarkupHelper ...@@ -74,7 +74,7 @@ module MarkupHelper
context[:project] ||= @project context[:project] ||= @project
html = markdown_unsafe(text, context) html = markdown_unsafe(text, context)
banzai_postprocess(html, context) prepare_for_rendering(html, context)
end end
def markdown_field(object, field) def markdown_field(object, field)
...@@ -82,13 +82,13 @@ module MarkupHelper ...@@ -82,13 +82,13 @@ module MarkupHelper
return '' unless object.present? return '' unless object.present?
html = Banzai.render_field(object, field) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field)) prepare_for_rendering(html, object.banzai_render_context(field))
end end
def markup(file_name, text, context = {}) def markup(file_name, text, context = {})
context[:project] ||= @project context[:project] ||= @project
html = context.delete(:rendered) || markup_unsafe(file_name, text, context) html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
banzai_postprocess(html, context) prepare_for_rendering(html, context)
end end
def render_wiki_content(wiki_page) def render_wiki_content(wiki_page)
...@@ -107,14 +107,14 @@ module MarkupHelper ...@@ -107,14 +107,14 @@ module MarkupHelper
wiki_page.formatted_content.html_safe wiki_page.formatted_content.html_safe
end end
banzai_postprocess(html, context) prepare_for_rendering(html, context)
end end
def markup_unsafe(file_name, text, context = {}) def markup_unsafe(file_name, text, context = {})
return '' unless text.present? return '' unless text.present?
if gitlab_markdown?(file_name) if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context)) markdown_unsafe(text, context)
elsif asciidoc?(file_name) elsif asciidoc?(file_name)
asciidoc_unsafe(text) asciidoc_unsafe(text)
elsif plain?(file_name) elsif plain?(file_name)
...@@ -225,8 +225,7 @@ module MarkupHelper ...@@ -225,8 +225,7 @@ module MarkupHelper
Gitlab::OtherMarkup.render(file_name, text) Gitlab::OtherMarkup.render(file_name, text)
end end
# Calls Banzai.post_process with some common context options def prepare_for_rendering(html, context = {})
def banzai_postprocess(html, context = {})
return '' unless html.present? return '' unless html.present?
context.merge!( context.merge!(
...@@ -239,7 +238,9 @@ module MarkupHelper ...@@ -239,7 +238,9 @@ module MarkupHelper
requested_path: @path requested_path: @path
) )
Banzai.post_process(html, context) html = Banzai.post_process(html, context)
Hamlit::RailsHelpers.preserve(html)
end end
extend self extend self
......
module MergeRequestsHelper module MergeRequestsHelper
def new_mr_path_from_push_event(event) def new_mr_path_from_push_event(event)
target_project = event.project.forked_from_project || event.project target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path( new_namespace_project_merge_request_path(
event.project.namespace, event.project.namespace,
event.project, event.project,
...@@ -127,6 +127,10 @@ module MergeRequestsHelper ...@@ -127,6 +127,10 @@ module MergeRequestsHelper
end end
end end
def target_projects(project)
[project, project.default_merge_request_target].uniq
end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end end
......
...@@ -8,6 +8,14 @@ module SnippetsHelper ...@@ -8,6 +8,14 @@ module SnippetsHelper
end end
end end
def download_snippet_path(snippet)
if snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
else
raw_snippet_path(snippet, inline: false)
end
end
# Return the path of a snippets index for a user or for a project # Return the path of a snippets index for a user or for a project
# #
# @returns String, path to snippet index # @returns String, path to snippet index
......
...@@ -27,6 +27,8 @@ class Blob < SimpleDelegator ...@@ -27,6 +27,8 @@ class Blob < SimpleDelegator
BlobViewer::Image, BlobViewer::Image,
BlobViewer::Sketch, BlobViewer::Sketch,
BlobViewer::Video,
BlobViewer::PDF, BlobViewer::PDF,
BlobViewer::BinarySTL, BlobViewer::BinarySTL,
......
module BlobViewer
class Video < Base
include Rich
include ClientSide
self.partial_name = 'video'
self.extensions = UploaderHelper::VIDEO_EXT
self.binary = true
self.switcher_icon = 'film'
self.switcher_title = 'video'
end
end
...@@ -78,6 +78,9 @@ module CacheMarkdownField ...@@ -78,6 +78,9 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field) def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field)
cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil?
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false html_changed = attribute_changed?(html_field) || false
......
...@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork? validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name) where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
...@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def validate_target_project
return true if target_project.merge_requests_enabled?
errors.add :base, 'Target project has disabled merge requests'
end
def validate_fork def validate_fork
return true unless target_project && source_project return true unless target_project && source_project
return true if target_project == source_project return true if target_project == source_project
......
...@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base ...@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base
validates :path, validates :path,
presence: true, presence: true,
length: { maximum: 255 }, length: { maximum: 255 },
namespace: true dynamic_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
...@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base ...@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path) Project.inside_path(full_path)
end end
def has_parent?
parent.present?
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base ...@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message } message: Gitlab::Regex.project_name_regex_message }
validates :path, validates :path,
presence: true, presence: true,
project_path: true, dynamic_path: true,
length: { maximum: 255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex, format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id } validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import? validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
...@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base ...@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base
else else
update_attribute(name, value) update_attribute(name, value)
end end
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
end end
def pushes_since_gc def pushes_since_gc
...@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base ...@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base
namespace_id_changed? namespace_id_changed?
end end
def default_merge_request_target
if forked_from_project&.merge_requests_enabled?
forked_from_project
else
self
end
end
alias_method :name_with_namespace, :full_name alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
...@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base ...@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base
ContainerRepository.build_root_repository(self).has_tags? ContainerRepository.build_root_repository(self).has_tags?
end end
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
end
end end
...@@ -505,14 +505,8 @@ class Repository ...@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: [] cache_method :tag_names, fallback: []
def branch_count delegate :branch_count, :tag_count, to: :raw_repository
branches.size
end
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
def avatar def avatar
...@@ -961,15 +955,13 @@ class Repository ...@@ -961,15 +955,13 @@ class Repository
end end
def is_ancestor?(ancestor_id, descendant_id) def is_ancestor?(ancestor_id, descendant_id)
# NOTE: This feature is intentionally disabled until Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
# https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved if is_enabled
# Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| raw_repository.is_ancestor?(ancestor_id, descendant_id)
# if is_enabled else
# raw_repository.is_ancestor?(ancestor_id, descendant_id) merge_base_commit(ancestor_id, descendant_id) == ancestor_id
# else end
merge_base_commit(ancestor_id, descendant_id) == ancestor_id end
# end
# end
end end
def empty_repo? def empty_repo?
......
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Linguist::BlobHelper
include CacheMarkdownField include CacheMarkdownField
include Noteable include Noteable
include Participable include Participable
...@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base ...@@ -87,47 +86,26 @@ class Snippet < ActiveRecord::Base
] ]
end end
def data def blob
content @blob ||= Blob.decorate(SnippetBlob.new(self), nil)
end end
def hook_attrs def hook_attrs
attributes attributes
end end
def size
0
end
def file_name def file_name
super.to_s super.to_s
end end
# alias for compatibility with blobs and highlighting
def path
file_name
end
def name
file_name
end
def sanitized_file_name def sanitized_file_name
file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '') file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
end end
def mode
nil
end
def visibility_level_field def visibility_level_field
:visibility_level :visibility_level
end end
def no_highlighting?
content.lines.count > 1000
end
def notes_with_associations def notes_with_associations
notes.includes(:author) notes.includes(:author)
end end
......
class SnippetBlob
include Linguist::BlobHelper
attr_reader :snippet
def initialize(snippet)
@snippet = snippet
end
delegate :id, to: :snippet
def name
snippet.file_name
end
alias_method :path, :name
def size
data.bytesize
end
def data
snippet.content
end
def rendered_markup
return unless Gitlab::MarkupHelper.gitlab_markdown?(name)
Banzai.render_field(snippet, :content)
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
false
end
def lfs_oid
nil
end
def lfs_size
nil
end
def truncated?
false
end
end
...@@ -118,7 +118,7 @@ class User < ActiveRecord::Base ...@@ -118,7 +118,7 @@ class User < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, validates :username,
namespace: true, dynamic_path: true,
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
......
...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity ...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end end
end end
expose :created_at
expose :tag expose :tag
expose :last? expose :last?
expose :user, using: UserEntity expose :user, using: UserEntity
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity expose :deployable, using: BuildEntity
......
class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
...@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity ...@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity
expose :details_path expose :details_path
expose :favicon do |status| expose :favicon do |status|
ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico")) dir = 'ci_favicons'
dir = File.join(dir, 'dev') if Rails.env.development?
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end end
end end
...@@ -28,7 +28,7 @@ module MergeRequests ...@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project) return target_project if target_project.present? && can?(current_user, :read_project, target_project)
project.forked_from_project || project project.default_merge_request_target
end end
def find_target_branch def find_target_branch
......
...@@ -281,7 +281,7 @@ class TodoService ...@@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target) def attributes_for_target(target)
attributes = { attributes = {
project_id: target.project.id, project_id: target&.project&.id,
target_id: target.id, target_id: target.id,
target_type: target.class.name, target_type: target.class.name,
commit_id: nil commit_id: nil
......
# DynamicPathValidator
#
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of it's parent.
GROUP_ROUTES = %w[
activity
avatar
edit
group_members
issues
labels
merge_requests
milestones
projects
subgroups
].freeze
CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
def self.without_reserved_wildcard_paths_regex
@without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES)
end
def self.without_reserved_child_paths_regex
@without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES)
end
# This is used to validate a full path.
# It doesn't match paths
# - Starting with one of the top level words
# - Containing one of the child level words in the middle of a path
def self.regex_excluding_child_paths(child_routes)
reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES)
not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))}
reserved_child_level_words = Regexp.union(child_routes)
not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))}
%r{#{not_starting_in_reserved_word}
#{not_containing_reserved_child}
#{Gitlab::Regex.full_namespace_regex}}x
end
def self.valid?(path)
path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path)
end
def self.full_path_reserved?(path)
path = path.to_s.downcase
_project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse)
wildcard_reserved?(path) || child_reserved?(namespace_parts)
end
def self.child_reserved?(path)
return false unless path
path !~ without_reserved_child_paths_regex
end
def self.wildcard_reserved?(path)
return false unless path
path !~ without_reserved_wildcard_paths_regex
end
delegate :full_path_reserved?,
:child_reserved?,
to: :class
def path_reserved_for_record?(record, value)
full_path = record.respond_to?(:full_path) ? record.full_path : value
# For group paths the entire path cannot contain a reserved child word
# The path doesn't contain the last `_project_part` so we need to validate
# if the entire path.
# Example:
# A *group* with full path `parent/activity` is reserved.
# A *project* with full path `parent/activity` is allowed.
if record.is_a? Group
child_reserved?(full_path)
else
full_path_reserved?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
return
end
if path_reserved_for_record?(record, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value, strict: false)
if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
strict = record.is_a?(Group) && record.parent_id
if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# ProjectPathValidator
#
# Custom validator for GitLab project path values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class ProjectPathValidator < ActiveModel::EachValidator
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
end
delegate :reserved?, to: :class
def validate_each(record, attribute, value)
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
...@@ -73,6 +73,12 @@ ...@@ -73,6 +73,12 @@
= container_reg = container_reg
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled = boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
%span.light.pull-right
= boolean_to_icon gitlab_pages_enabled
.col-md-4 .col-md-4
%h4 %h4
......
= form_errors(hook)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group
= form.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- page_title 'Edit System Hook'
%h3.page-title
Edit System Hook
%p.light
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
- page_title "System Hooks" - page_title 'System Hooks'
%h3.page-title %h3.page-title
System hooks System hooks
%p.light %p.light
#{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project. used for binding events when GitLab creates a User or Project.
%hr %hr
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= form_errors(@hook) = render partial: 'form', locals: { form: f, hook: @hook }
.form-group
= f.label :url, 'URL', class: 'control-label'
.col-sm-10
= f.text_field :url, class: 'form-control'
.form-group
= f.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions .form-actions
= f.submit "Add system hook", class: "btn btn-create" = f.submit 'Add system hook', class: 'btn btn-create'
%hr %hr
- if @hooks.any? - if @hooks.any?
...@@ -62,11 +22,12 @@ ...@@ -62,11 +22,12 @@
- @hooks.each do |hook| - @hooks.each do |hook|
%li %li
.controls .controls
= link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url .monospace= hook.url
%div %div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger) - if hook.send(trigger)
%span.label.label-gray= trigger.titleize %span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
%ul.nav-links %ul.nav-links
= nav_link(page: dashboard_groups_path) do = nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups' do = link_to dashboard_groups_path, title: 'Your groups' do
Your Groups Your groups
= nav_link(page: explore_groups_path) do = nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore groups' do = link_to explore_groups_path, title: 'Explore public groups' do
Explore Groups Explore public groups
.nav-controls .nav-controls
= render 'shared/groups/search_form' = render 'shared/groups/search_form'
= render 'shared/groups/dropdown' = render 'shared/groups/dropdown'
......
.discussion-notes .discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } } %ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note = render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user - if current_user
.discussion-reply-holder .discussion-reply-holder
......
...@@ -7,6 +7,15 @@ ...@@ -7,6 +7,15 @@
= render 'explore/head' = render 'explore/head'
= render 'nav' = render 'nav'
- if cookies[:explore_groups_landing_dismissed] != 'true'
.explore-groups.landing.content-block.js-explore-groups-landing.hidden
%button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
.svg-container
= custom_icon('icon_explore_groups_splash')
.inner-content
%p Below you will find all the groups that are public.
%p You can easily contribute to them by requesting to join these groups.
- if @groups.present? - if @groups.present?
= render 'groups' = render 'groups'
- else - else
......
...@@ -27,8 +27,7 @@ ...@@ -27,8 +27,7 @@
.row .row
.col-md-8 .col-md-8
.documentation-index .documentation-index
= preserve do = markdown(@help_index)
= markdown(@help_index)
.col-md-4 .col-md-4
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
......
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
%div{ class: container_class } %div{ class: container_class }
.wiki-holder.prepend-top-default.append-bottom-default .wiki-holder.prepend-top-default.append-bottom-default
.wiki .wiki
= preserve do = render_wiki_content(@wiki_home)
= render_wiki_content(@wiki_home)
- else - else
- can_create_wiki = can?(current_user, :create_wiki, @project) - can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
......
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
= render 'projects/blob/viewer_switcher', blob: blob unless blame = render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false) = copy_blob_source_button(blob) unless blame
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id)) = open_raw_blob_button(blob)
= view_on_environment_button(@commit.sha, @path, @environment) if @environment = view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
......
- blob = viewer.blob - blob = viewer.blob
- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup)
.file-content.wiki .file-content.wiki
= markup(blob.name, blob.data) = markup(blob.name, blob.data, rendered: rendered_markup)
.file-content.video
%video{ src: blob_raw_url, controls: true, data: { setup: '{}' } }
...@@ -28,8 +28,9 @@ ...@@ -28,8 +28,9 @@
":value" => "issue.assignee.id", ":value" => "issue.assignee.id",
"v-if" => "issue.assignee" } "v-if" => "issue.assignee" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee Select assignee
= icon("chevron-down") = icon("chevron-down")
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
git init git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add . git add .
git commit git commit -m "Initial commit"
git push -u origin master git push -u origin master
%fieldset %fieldset
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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