Merge remote-tracking branch 'origin/master' into add-sentry-js-again-with-vue
This commit is contained in:
commit
3a3cf07f93
|
@ -11,6 +11,7 @@ variables:
|
|||
NODE_ENV: "test"
|
||||
SIMPLECOV: "true"
|
||||
GIT_DEPTH: "20"
|
||||
GIT_SUBMODULE_STRATEGY: "none"
|
||||
PHANTOMJS_VERSION: "2.1.1"
|
||||
GET_SOURCES_ATTEMPTS: "3"
|
||||
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
|
||||
|
@ -402,13 +403,6 @@ docs:check:links:
|
|||
# Check the internal links
|
||||
- bundle exec nanoc check internal_links
|
||||
|
||||
bundler:check:
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *ruby-static-analysis
|
||||
script:
|
||||
- bundle check
|
||||
|
||||
bundler:audit:
|
||||
stage: test
|
||||
<<: *ruby-static-analysis
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
filtered by the "regression" or "bug" label:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
|
||||
Please remove this notice if you're confident your issue isn't a duplicate.
|
||||
|
||||
------
|
||||
|
||||
### Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
@ -26,6 +40,7 @@ logs, and code as it's very hard to read otherwise.)
|
|||
#### Results of GitLab environment info
|
||||
|
||||
<details>
|
||||
<pre>
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:env:info`)
|
||||
|
@ -33,11 +48,13 @@ logs, and code as it's very hard to read otherwise.)
|
|||
(For installations from source run and paste the output of:
|
||||
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
|
||||
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
#### Results of GitLab application Check
|
||||
|
||||
<details>
|
||||
<pre>
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:check SANITIZE=true`)
|
||||
|
@ -47,8 +64,11 @@ logs, and code as it's very hard to read otherwise.)
|
|||
|
||||
(we will only investigate if the tests are passing)
|
||||
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
### Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
/label ~bug
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
Please read this!
|
||||
|
||||
Before opening a new issue, make sure to search for keywords in the issues
|
||||
filtered by the "feature proposal" label:
|
||||
|
||||
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
|
||||
|
||||
and verify the issue you're about to submit isn't a duplicate.
|
||||
|
||||
Please remove this notice if you're confident your issue isn't a duplicate.
|
||||
|
||||
------
|
||||
|
||||
### Description
|
||||
|
||||
(Include problem, use cases, benefits, and/or goals)
|
||||
|
@ -15,3 +28,5 @@
|
|||
3. How does someone use this
|
||||
|
||||
During implementation, this can then be copied and used as a starter for the documentation.)
|
||||
|
||||
/label ~"feature proposal"
|
||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -2,6 +2,21 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 9.1.1 (2017-04-26)
|
||||
|
||||
- Add a transaction around move_issues_to_ghost_user. !10465
|
||||
- Properly expire cache for all MRs of a pipeline. !10770
|
||||
- Add sub-nav for Project Integration Services edit page. !10813
|
||||
- Fix missing duration for blocked pipelines. !10856
|
||||
- Fix lastest commit status text on main project page. !10863
|
||||
- Add index on ci_builds.updated_at. !10870 (blackst0ne)
|
||||
- Fix 500 error due to trying to show issues from pending deleting projects. !10906
|
||||
- Ensures that OAuth/LDAP/SAML users don't need to be confirmed.
|
||||
- Ensure replying to an individual note by email creates a note with its own discussion ID.
|
||||
- Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled.
|
||||
- Fix usage ping docs link from empty cohorts page.
|
||||
- Eliminate N+1 queries in loading namespaces for every issuable in milestones.
|
||||
|
||||
## 9.1.0 (2017-04-22)
|
||||
|
||||
- Added merge requests empty state. !7342
|
||||
|
|
9
Gemfile
9
Gemfile
|
@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres
|
|||
|
||||
gem 'rugged', '~> 0.25.1.1'
|
||||
|
||||
gem 'faraday', '~> 0.11.0'
|
||||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'doorkeeper', '~> 4.2.0'
|
||||
|
@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0'
|
|||
gem 'acts-as-taggable-on', '~> 4.0'
|
||||
|
||||
# Background jobs
|
||||
gem 'sidekiq', '~> 4.2.7'
|
||||
gem 'sidekiq', '~> 5.0'
|
||||
gem 'sidekiq-cron', '~> 0.4.4'
|
||||
gem 'redis-namespace', '~> 1.5.2'
|
||||
gem 'sidekiq-limit_fetch', '~> 3.4'
|
||||
|
@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
|
|||
gem 'slack-notifier', '~> 1.5.1'
|
||||
|
||||
# Asana integration
|
||||
gem 'asana', '~> 0.4.0'
|
||||
gem 'asana', '~> 0.6.0'
|
||||
|
||||
# FogBugz integration
|
||||
gem 'ruby-fogbugz', '~> 0.2.1'
|
||||
|
@ -291,6 +293,7 @@ group :development, :test do
|
|||
gem 'spinach-rails', '~> 0.2.1'
|
||||
gem 'spinach-rerun-reporter', '~> 0.0.2'
|
||||
gem 'rspec_profiling', '~> 0.0.5'
|
||||
gem 'rspec-set', '~> 0.1.3'
|
||||
|
||||
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
|
||||
gem 'minitest', '~> 5.7.0'
|
||||
|
@ -345,7 +348,7 @@ gem 'html2text'
|
|||
gem 'ruby-prof', '~> 0.16.2'
|
||||
|
||||
# OAuth
|
||||
gem 'oauth2', '~> 1.2.0'
|
||||
gem 'oauth2', '~> 1.3.0'
|
||||
|
||||
# Soft deletion
|
||||
gem 'paranoia', '~> 2.2'
|
||||
|
|
31
Gemfile.lock
31
Gemfile.lock
|
@ -47,7 +47,7 @@ GEM
|
|||
akismet (2.0.0)
|
||||
allocations (1.0.5)
|
||||
arel (6.0.4)
|
||||
asana (0.4.0)
|
||||
asana (0.6.0)
|
||||
faraday (~> 0.9)
|
||||
faraday_middleware (~> 0.9)
|
||||
faraday_middleware-multi_json (~> 0.0)
|
||||
|
@ -193,10 +193,10 @@ GEM
|
|||
factory_girl_rails (4.7.0)
|
||||
factory_girl (~> 4.7.0)
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.9.2)
|
||||
faraday (0.11.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (0.10.0)
|
||||
faraday (>= 0.7.4, < 0.10)
|
||||
faraday_middleware (0.11.0.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
faraday_middleware-multi_json (0.0.6)
|
||||
faraday_middleware
|
||||
multi_json
|
||||
|
@ -429,7 +429,7 @@ GEM
|
|||
multi_json (~> 1.10)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.4)
|
||||
mail (2.6.5)
|
||||
mime-types (>= 1.16, < 4)
|
||||
mail_room (0.9.1)
|
||||
memoist (0.15.0)
|
||||
|
@ -454,15 +454,15 @@ GEM
|
|||
mini_portile2 (~> 2.1.0)
|
||||
numerizer (0.1.1)
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.2.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
oauth2 (1.3.1)
|
||||
faraday (>= 0.8, < 0.12)
|
||||
jwt (~> 1.0)
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
octokit (4.6.2)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
oj (2.17.4)
|
||||
oj (2.17.5)
|
||||
omniauth (1.4.2)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (>= 1.0, < 3)
|
||||
|
@ -603,7 +603,7 @@ GEM
|
|||
json
|
||||
recursive-open-struct (1.0.0)
|
||||
redcarpet (3.4.0)
|
||||
redis (3.2.2)
|
||||
redis (3.3.3)
|
||||
redis-actionpack (5.0.1)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
|
@ -659,6 +659,7 @@ GEM
|
|||
rspec-support (~> 3.5.0)
|
||||
rspec-retry (0.4.5)
|
||||
rspec-core
|
||||
rspec-set (0.1.3)
|
||||
rspec-support (3.5.0)
|
||||
rspec_profiling (0.0.5)
|
||||
activerecord
|
||||
|
@ -716,11 +717,11 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (4.2.10)
|
||||
sidekiq (5.0.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
redis (~> 3.3, >= 3.3.3)
|
||||
sidekiq-cron (0.4.4)
|
||||
redis-namespace (>= 1.5.2)
|
||||
rufus-scheduler (>= 2.0.24)
|
||||
|
@ -853,7 +854,7 @@ DEPENDENCIES
|
|||
after_commit_queue (~> 1.3.0)
|
||||
akismet (~> 2.0)
|
||||
allocations (~> 1.0)
|
||||
asana (~> 0.4.0)
|
||||
asana (~> 0.6.0)
|
||||
asciidoctor (~> 1.5.2)
|
||||
asciidoctor-plantuml (= 0.0.7)
|
||||
attr_encrypted (~> 3.0.0)
|
||||
|
@ -891,6 +892,7 @@ DEPENDENCIES
|
|||
email_reply_trimmer (~> 0.1)
|
||||
email_spec (~> 1.6.0)
|
||||
factory_girl_rails (~> 4.7.0)
|
||||
faraday (~> 0.11.0)
|
||||
ffaker (~> 2.4)
|
||||
flay (~> 2.8.0)
|
||||
fog-aws (~> 0.9)
|
||||
|
@ -943,7 +945,7 @@ DEPENDENCIES
|
|||
mysql2 (~> 0.3.16)
|
||||
net-ssh (~> 3.0.1)
|
||||
nokogiri (~> 1.6.7, >= 1.6.7.2)
|
||||
oauth2 (~> 1.2.0)
|
||||
oauth2 (~> 1.3.0)
|
||||
octokit (~> 4.6.2)
|
||||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.4.2)
|
||||
|
@ -988,6 +990,7 @@ DEPENDENCIES
|
|||
rqrcode-rails3 (~> 0.1.7)
|
||||
rspec-rails (~> 3.5.0)
|
||||
rspec-retry (~> 0.4.5)
|
||||
rspec-set (~> 0.1.3)
|
||||
rspec_profiling (~> 0.0.5)
|
||||
rubocop (~> 0.47.1)
|
||||
rubocop-rspec (~> 1.15.0)
|
||||
|
@ -1004,7 +1007,7 @@ DEPENDENCIES
|
|||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
sidekiq (~> 4.2.7)
|
||||
sidekiq (~> 5.0)
|
||||
sidekiq-cron (~> 0.4.4)
|
||||
sidekiq-limit_fetch (~> 3.4)
|
||||
simplecov (~> 0.14.0)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
|
||||
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
|
||||
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Test coverage
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ export default () => {
|
|||
},
|
||||
},
|
||||
template: `
|
||||
<div class="container-fluid md prepend-top-default append-bottom-default">
|
||||
<div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
|
||||
<div
|
||||
class="text-center loading"
|
||||
v-if="loading && !error">
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/* global Flash */
|
||||
export default class BlobViewer {
|
||||
constructor() {
|
||||
this.switcher = document.querySelector('.js-blob-viewer-switcher');
|
||||
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
|
||||
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
|
||||
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
|
||||
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
|
||||
this.$blobContentHolder = $('#blob-content-holder');
|
||||
|
||||
let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
|
||||
|
||||
this.initBindings();
|
||||
|
||||
if (this.switcher && location.hash.indexOf('#L') === 0) {
|
||||
initialViewerName = 'simple';
|
||||
}
|
||||
|
||||
this.switchToViewer(initialViewerName);
|
||||
}
|
||||
|
||||
initBindings() {
|
||||
if (this.switcherBtns.length) {
|
||||
Array.from(this.switcherBtns)
|
||||
.forEach((el) => {
|
||||
el.addEventListener('click', this.switchViewHandler.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.copySourceBtn) {
|
||||
this.copySourceBtn.addEventListener('click', () => {
|
||||
if (this.copySourceBtn.classList.contains('disabled')) return;
|
||||
|
||||
this.switchToViewer('simple');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switchViewHandler(e) {
|
||||
const target = e.currentTarget;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.switchToViewer(target.getAttribute('data-viewer'));
|
||||
}
|
||||
|
||||
toggleCopyButtonState() {
|
||||
if (!this.copySourceBtn) return;
|
||||
|
||||
if (this.simpleViewer.getAttribute('data-loaded')) {
|
||||
this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
|
||||
this.copySourceBtn.classList.remove('disabled');
|
||||
} else if (this.activeViewer === this.simpleViewer) {
|
||||
this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard');
|
||||
this.copySourceBtn.classList.add('disabled');
|
||||
} else {
|
||||
this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
|
||||
this.copySourceBtn.classList.add('disabled');
|
||||
}
|
||||
|
||||
$(this.copySourceBtn).tooltip('fixTitle');
|
||||
}
|
||||
|
||||
loadViewer(viewerParam) {
|
||||
const viewer = viewerParam;
|
||||
const url = viewer.getAttribute('data-url');
|
||||
|
||||
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewer.setAttribute('data-loading', 'true');
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
dataType: 'JSON',
|
||||
})
|
||||
.fail(() => new Flash('Error loading source view'))
|
||||
.done((data) => {
|
||||
viewer.innerHTML = data.html;
|
||||
$(viewer).syntaxHighlight();
|
||||
|
||||
viewer.setAttribute('data-loaded', 'true');
|
||||
|
||||
this.$blobContentHolder.trigger('highlight:line');
|
||||
|
||||
this.toggleCopyButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
switchToViewer(name) {
|
||||
const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
|
||||
if (this.activeViewer === newViewer) return;
|
||||
|
||||
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
|
||||
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
|
||||
const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
|
||||
|
||||
if (oldButton) {
|
||||
oldButton.classList.remove('active');
|
||||
}
|
||||
|
||||
if (newButton) {
|
||||
newButton.classList.add('active');
|
||||
newButton.blur();
|
||||
}
|
||||
|
||||
if (oldViewer) {
|
||||
oldViewer.classList.add('hidden');
|
||||
}
|
||||
|
||||
newViewer.classList.remove('hidden');
|
||||
|
||||
this.activeViewer = newViewer;
|
||||
|
||||
this.toggleCopyButtonState();
|
||||
|
||||
this.loadViewer(newViewer);
|
||||
}
|
||||
}
|
|
@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
|
|||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length &&
|
||||
this.$children &&
|
||||
!this.isMakingRequest &&
|
||||
!this.isLoading) {
|
||||
this.store.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
},
|
||||
|
|
|
@ -48,6 +48,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
import UserCallout from './user_callout';
|
||||
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
|
||||
import ShortcutsWiki from './shortcuts_wiki';
|
||||
import BlobViewer from './blob/viewer/index';
|
||||
|
||||
const ShortcutsBlob = require('./shortcuts_blob');
|
||||
|
||||
|
@ -299,6 +300,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
|
|||
gl.TargetBranchDropDown.bootstrap();
|
||||
break;
|
||||
case 'projects:blob:show':
|
||||
new BlobViewer();
|
||||
gl.TargetBranchDropDown.bootstrap();
|
||||
initBlob();
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
|
||||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsService from '../services/environments_service';
|
||||
import EnvironmentTable from './environments_table.vue';
|
||||
import EnvironmentsStore from '../stores/environments_store';
|
||||
|
@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
|
|||
import '../../lib/utils/common_utils';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default Vue.component('environment-component', {
|
||||
export default {
|
||||
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
|
@ -140,76 +141,90 @@ export default Vue.component('environment-component', {
|
|||
});
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area">
|
||||
<ul v-if="!isLoading" class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
|
||||
<a :href="newEnvironmentPath" class="btn btn-create">
|
||||
New environment
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area">
|
||||
<ul
|
||||
v-if="!isLoading"
|
||||
class="nav-links">
|
||||
<li :class="{ active: scope === null || scope === 'available' }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-list environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
<br />
|
||||
<a :href="helpPagePath">
|
||||
Read more about environments
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create js-new-environment-button">
|
||||
New Environment
|
||||
</li>
|
||||
<li :class="{ active : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
</div>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation">
|
||||
</table-pagination>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-if="canCreateEnvironmentParsed && !isLoading"
|
||||
class="nav-controls">
|
||||
<a
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create">
|
||||
New environment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
<div class="content-list environments-container">
|
||||
<div
|
||||
class="environments-list-loading text-center"
|
||||
v-if="isLoading">
|
||||
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
<br />
|
||||
<a :href="helpPagePath">
|
||||
Read more about environments
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a
|
||||
v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create js-new-environment-button">
|
||||
New Environment
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
</div>
|
||||
|
||||
<table-pagination
|
||||
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,13 +1,10 @@
|
|||
import EnvironmentsComponent from './components/environment';
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsComponent from './components/environment.vue';
|
||||
|
||||
$(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
if (gl.EnvironmentsListApp) {
|
||||
gl.EnvironmentsListApp.$destroy(true);
|
||||
}
|
||||
|
||||
gl.EnvironmentsListApp = new EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => new Vue({
|
||||
el: '#environments-list-view',
|
||||
components: {
|
||||
'environments-table-app': EnvironmentsComponent,
|
||||
},
|
||||
render: createElement => createElement('environments-table-app'),
|
||||
}));
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import EnvironmentsFolderComponent from './environments_folder_view';
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsFolderComponent from './environments_folder_view.vue';
|
||||
|
||||
$(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
if (gl.EnvironmentsListFolderApp) {
|
||||
gl.EnvironmentsListFolderApp.$destroy(true);
|
||||
}
|
||||
|
||||
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
|
||||
el: document.querySelector('#environments-folder-list-view'),
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => new Vue({
|
||||
el: '#environments-folder-list-view',
|
||||
components: {
|
||||
'environments-folder-app': EnvironmentsFolderComponent,
|
||||
},
|
||||
render: createElement => createElement('environments-folder-app'),
|
||||
}));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
import Vue from 'vue';
|
||||
import EnvironmentsService from '../services/environments_service';
|
||||
import EnvironmentTable from '../components/environments_table.vue';
|
||||
import EnvironmentsStore from '../stores/environments_store';
|
||||
|
@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
|
|||
import '../../lib/utils/common_utils';
|
||||
import '../../vue_shared/vue_resource_interceptor';
|
||||
|
||||
export default Vue.component('environment-folder-view', {
|
||||
export default {
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
'table-pagination': TablePaginationComponent,
|
||||
|
@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', {
|
|||
return param;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cssContainerClass">
|
||||
<div
|
||||
class="top-area"
|
||||
v-if="!isLoading">
|
||||
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area" v-if="!isLoading">
|
||||
<h4 class="js-folder-name environments-folder-name">
|
||||
Environments / <b>{{folderName}}</b>
|
||||
</h4>
|
||||
|
||||
<h4 class="js-folder-name environments-folder-name">
|
||||
Environments / <b>{{folderName}}</b>
|
||||
</h4>
|
||||
<ul class="nav-links">
|
||||
<li :class="{ active: scope === null || scope === 'available' }">
|
||||
<a
|
||||
:href="availablePath"
|
||||
class="js-available-environments-folder-tab">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="{ active : scope === 'stopped' }">
|
||||
<a
|
||||
:href="stoppedPath"
|
||||
class="js-stopped-environments-folder-tab">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
|
||||
<a :href="availablePath" class="js-available-environments-folder-tab">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="environments-container">
|
||||
<div
|
||||
class="environments-list-loading text-center"
|
||||
v-if="isLoading">
|
||||
<i
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true"/>
|
||||
</div>
|
||||
|
||||
<div class="environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"/>
|
||||
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"/>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation"/>
|
||||
</div>
|
||||
<table-pagination
|
||||
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation"/>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
</div>
|
||||
</template>
|
|
@ -77,13 +77,14 @@ class FilteredSearchManager {
|
|||
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
||||
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
||||
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
||||
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
|
||||
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
|
||||
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
||||
this.editTokenWrapper = this.editToken.bind(this);
|
||||
this.tokenChange = this.tokenChange.bind(this);
|
||||
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
||||
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
||||
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
||||
this.removeTokenWrapper = this.removeToken.bind(this);
|
||||
|
||||
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
||||
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
||||
|
@ -96,12 +97,13 @@ class FilteredSearchManager {
|
|||
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
||||
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
||||
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
||||
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
|
||||
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
||||
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
||||
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||
document.addEventListener('click', this.unselectEditTokensWrapper);
|
||||
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
||||
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||
}
|
||||
|
||||
|
@ -117,12 +119,13 @@ class FilteredSearchManager {
|
|||
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
||||
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
||||
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
||||
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
|
||||
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
||||
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
||||
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
||||
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
||||
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||
}
|
||||
|
||||
|
@ -195,14 +198,28 @@ class FilteredSearchManager {
|
|||
|
||||
static selectToken(e) {
|
||||
const button = e.target.closest('.selectable');
|
||||
const removeButtonSelected = e.target.closest('.remove-token');
|
||||
|
||||
if (button) {
|
||||
if (!removeButtonSelected && button) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
gl.FilteredSearchVisualTokens.selectToken(button);
|
||||
}
|
||||
}
|
||||
|
||||
removeToken(e) {
|
||||
const removeButtonSelected = e.target.closest('.remove-token');
|
||||
|
||||
if (removeButtonSelected) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const button = e.target.closest('.selectable');
|
||||
gl.FilteredSearchVisualTokens.selectToken(button, true);
|
||||
this.removeSelectedToken();
|
||||
}
|
||||
}
|
||||
|
||||
unselectEditTokens(e) {
|
||||
const inputContainer = this.container.querySelector('.filtered-search-box');
|
||||
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
||||
|
@ -248,16 +265,21 @@ class FilteredSearchManager {
|
|||
}
|
||||
}
|
||||
|
||||
removeSelectedToken(e) {
|
||||
removeSelectedTokenKeydown(e) {
|
||||
// 8 = Backspace Key
|
||||
// 46 = Delete Key
|
||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||
this.handleInputPlaceholder();
|
||||
this.toggleClearSearchButton();
|
||||
this.removeSelectedToken();
|
||||
}
|
||||
}
|
||||
|
||||
removeSelectedToken() {
|
||||
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||
this.handleInputPlaceholder();
|
||||
this.toggleClearSearchButton();
|
||||
this.dropdownManager.updateCurrentDropdownOffset();
|
||||
}
|
||||
|
||||
onClearSearch(e) {
|
||||
e.preventDefault();
|
||||
this.clearSearch();
|
||||
|
|
|
@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
|
|||
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
||||
}
|
||||
|
||||
static selectToken(tokenButton) {
|
||||
static selectToken(tokenButton, forceSelection = false) {
|
||||
const selected = tokenButton.classList.contains('selected');
|
||||
FilteredSearchVisualTokens.unselectTokens();
|
||||
|
||||
if (!selected) {
|
||||
if (!selected || forceSelection) {
|
||||
tokenButton.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
|
|||
return `
|
||||
<div class="selectable" role="button">
|
||||
<div class="name"></div>
|
||||
<div class="value"></div>
|
||||
<div class="value-container">
|
||||
<div class="value"></div>
|
||||
<div class="remove-token" role="button">
|
||||
<i class="fa fa-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
|
|||
|
||||
if (value) {
|
||||
const button = lastVisualToken.querySelector('.selectable');
|
||||
button.removeChild(value);
|
||||
const valueContainer = lastVisualToken.querySelector('.value-container');
|
||||
button.removeChild(valueContainer);
|
||||
lastVisualToken.innerHTML = button.innerHTML;
|
||||
} else {
|
||||
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import emojiMap from 'emojis/digests.json';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { glEmojiTag } from '~/behaviors/gl_emoji';
|
||||
import glRegexp from '~/lib/utils/regexp';
|
||||
|
||||
// Creates the variables for setting up GFM auto-completion
|
||||
window.gl = window.gl || {};
|
||||
|
@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
|
|||
callbacks: {
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
filter: this.DefaultOptions.filter
|
||||
filter: this.DefaultOptions.filter,
|
||||
|
||||
matcher: (flag, subtext) => {
|
||||
const relevantText = subtext.trim().split(/\s/).pop();
|
||||
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
|
||||
const match = regexp.exec(relevantText);
|
||||
|
||||
return match && match.length ? match[1] : null;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Team Members
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Regexp utility for the convenience of working with regular expressions.
|
||||
*
|
||||
*/
|
||||
|
||||
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
|
||||
// Unicode 6.1
|
||||
const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
|
||||
|
||||
export default { unicodeLetters };
|
|
@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo');
|
|||
LineHighlighter.prototype._hash = '';
|
||||
|
||||
function LineHighlighter(hash) {
|
||||
var range;
|
||||
if (hash == null) {
|
||||
// Initialize a LineHighlighter object
|
||||
//
|
||||
|
@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo');
|
|||
this.setHash = bind(this.setHash, this);
|
||||
this.highlightLine = bind(this.highlightLine, this);
|
||||
this.clickHandler = bind(this.clickHandler, this);
|
||||
this.highlightHash = this.highlightHash.bind(this);
|
||||
this._hash = hash;
|
||||
this.bindEvents();
|
||||
if (hash !== '') {
|
||||
range = this.hashToRange(hash);
|
||||
this.highlightHash();
|
||||
}
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
const $blobContentHolder = $('#blob-content-holder');
|
||||
$blobContentHolder.on('click', 'a[data-line-number]', this.clickHandler);
|
||||
$blobContentHolder.on('highlight:line', this.highlightHash);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.highlightHash = function() {
|
||||
var range;
|
||||
if (this._hash !== '') {
|
||||
range = this.hashToRange(this._hash);
|
||||
if (range[0]) {
|
||||
this.highlightRange(range);
|
||||
$.scrollTo("#L" + range[0], {
|
||||
|
@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo');
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LineHighlighter.prototype.bindEvents = function() {
|
||||
$('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler);
|
||||
};
|
||||
|
||||
LineHighlighter.prototype.clickHandler = function(event) {
|
||||
|
|
|
@ -28,7 +28,9 @@ export default class MiniPipelineGraph {
|
|||
* All dropdown events are fired at the .dropdown-menu's parent element.
|
||||
*/
|
||||
bindEvents() {
|
||||
$(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList);
|
||||
$(document)
|
||||
.off('shown.bs.dropdown', this.container)
|
||||
.on('shown.bs.dropdown', this.container, this.getBuildsList);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,6 +93,9 @@ export default class MiniPipelineGraph {
|
|||
},
|
||||
error: () => {
|
||||
this.toggleLoading(button);
|
||||
if ($(button).parent().hasClass('open')) {
|
||||
$(button).dropdown('toggle');
|
||||
}
|
||||
new Flash('An error occurred while fetching the builds.', 'alert');
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,13 +2,6 @@
|
|||
import StatusIconEntityMap from '../../ci_status_icons';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
builds: '',
|
||||
spinner: '<span class="fa fa-spinner fa-spin"></span>',
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
|
@ -16,6 +9,13 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
builds: '',
|
||||
spinner: '<span class="fa fa-spinner fa-spin"></span>',
|
||||
};
|
||||
},
|
||||
|
||||
updated() {
|
||||
if (this.builds) {
|
||||
this.stopDropdownClickPropagation();
|
||||
|
@ -31,7 +31,13 @@ export default {
|
|||
return this.$http.get(this.stage.dropdown_path)
|
||||
.then((response) => {
|
||||
this.builds = JSON.parse(response.body).html;
|
||||
}, () => {
|
||||
})
|
||||
.catch(() => {
|
||||
// If dropdown is opened we'll close it.
|
||||
if (this.$el.classList.contains('open')) {
|
||||
$(this.$refs.dropdown).dropdown('toggle');
|
||||
}
|
||||
|
||||
const flash = new Flash('Something went wrong on our end.');
|
||||
return flash;
|
||||
});
|
||||
|
@ -46,9 +52,10 @@ export default {
|
|||
* target the click event of this component.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
|
||||
.on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -81,12 +88,22 @@ export default {
|
|||
data-placement="top"
|
||||
data-toggle="dropdown"
|
||||
type="button"
|
||||
:aria-label="stage.title">
|
||||
<span v-html="svgHTML" aria-hidden="true"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
:aria-label="stage.title"
|
||||
ref="dropdown">
|
||||
<span
|
||||
v-html="svgHTML"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
<i
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
|
||||
<div class="arrow-up" aria-hidden="true"></div>
|
||||
<ul
|
||||
ref="dropdown-content"
|
||||
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
|
||||
<div
|
||||
class="arrow-up"
|
||||
aria-hidden="true"></div>
|
||||
<div
|
||||
:class="dropdownClass"
|
||||
class="js-builds-dropdown-list scrollable-menu"
|
||||
|
|
|
@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
|
|||
import '../../lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
finishedTime: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentTime: new Date(),
|
||||
iconTimerSvg,
|
||||
};
|
||||
},
|
||||
props: ['pipeline'],
|
||||
|
||||
updated() {
|
||||
$(this.$refs.tooltip).tooltip('fixTitle');
|
||||
},
|
||||
|
||||
computed: {
|
||||
timeAgo() {
|
||||
return gl.utils.getTimeago();
|
||||
hasDuration() {
|
||||
return this.duration > 0;
|
||||
},
|
||||
|
||||
hasFinishedTime() {
|
||||
return this.finishedTime !== '';
|
||||
},
|
||||
|
||||
localTimeFinished() {
|
||||
return gl.utils.formatDate(this.pipeline.details.finished_at);
|
||||
return gl.utils.formatDate(this.finishedTime);
|
||||
},
|
||||
timeStopped() {
|
||||
const changeTime = this.currentTime;
|
||||
const options = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
};
|
||||
options.timeZoneName = 'short';
|
||||
const finished = this.pipeline.details.finished_at;
|
||||
if (!finished && changeTime) return false;
|
||||
return ({ words: this.timeAgo.format(finished) });
|
||||
},
|
||||
duration() {
|
||||
const { duration } = this.pipeline.details;
|
||||
const date = new Date(duration * 1000);
|
||||
|
||||
durationFormated() {
|
||||
const date = new Date(this.duration * 1000);
|
||||
|
||||
let hh = date.getUTCHours();
|
||||
let mm = date.getUTCMinutes();
|
||||
let ss = date.getSeconds();
|
||||
|
||||
if (hh < 10) hh = `0${hh}`;
|
||||
if (mm < 10) mm = `0${mm}`;
|
||||
if (ss < 10) ss = `0${ss}`;
|
||||
// left pad
|
||||
if (hh < 10) {
|
||||
hh = `0${hh}`;
|
||||
}
|
||||
if (mm < 10) {
|
||||
mm = `0${mm}`;
|
||||
}
|
||||
if (ss < 10) {
|
||||
ss = `0${ss}`;
|
||||
}
|
||||
|
||||
if (duration !== null) return `${hh}:${mm}:${ss}`;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeTime() {
|
||||
this.currentTime = new Date();
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
},
|
||||
|
||||
finishedTimeFormated() {
|
||||
const timeAgo = gl.utils.getTimeago();
|
||||
|
||||
return timeAgo.format(this.finishedTime);
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<td class="pipelines-time-ago">
|
||||
<p class="duration" v-if='duration'>
|
||||
<span v-html="iconTimerSvg"></span>
|
||||
{{duration}}
|
||||
<p
|
||||
class="duration"
|
||||
v-if="hasDuration">
|
||||
<span
|
||||
v-html="iconTimerSvg">
|
||||
</span>
|
||||
{{durationFormated}}
|
||||
</p>
|
||||
<p class="finished-at" v-if='timeStopped'>
|
||||
<i class="fa fa-calendar"></i>
|
||||
|
||||
<p
|
||||
class="finished-at"
|
||||
v-if="hasFinishedTime">
|
||||
|
||||
<i
|
||||
class="fa fa-calendar"
|
||||
aria-hidden="true" />
|
||||
|
||||
<time
|
||||
ref="tooltip"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
:data-original-title='localTimeFinished'>
|
||||
{{timeStopped.words}}
|
||||
:title="localTimeFinished">
|
||||
{{finishedTimeFormated}}
|
||||
</time>
|
||||
</p>
|
||||
</td>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import Visibility from 'visibilityjs';
|
||||
import PipelinesService from './services/pipelines_service';
|
||||
import eventHub from './event_hub';
|
||||
|
@ -161,15 +160,6 @@ export default {
|
|||
eventHub.$on('refreshPipelines', this.fetchPipelines);
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length &&
|
||||
this.$children &&
|
||||
!this.isMakingRequest &&
|
||||
!this.isLoading) {
|
||||
this.store.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('refreshPipelines');
|
||||
},
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* eslint-disable no-underscore-dangle*/
|
||||
import VueRealtimeListener from '../../vue_realtime_listener';
|
||||
|
||||
export default class PipelinesStore {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
|
@ -30,32 +27,4 @@ export default class PipelinesStore {
|
|||
|
||||
this.state.pageInfo = paginationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Move this inside the component.
|
||||
*
|
||||
* Once the data is received we will start the time ago loops.
|
||||
*
|
||||
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
|
||||
* update the time to show how long as passed.
|
||||
*
|
||||
*/
|
||||
startTimeAgoLoops() {
|
||||
const startTimeLoops = () => {
|
||||
this.timeLoopInterval = setInterval(() => {
|
||||
this.$children[0].$children.reduce((acc, component) => {
|
||||
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
|
||||
acc.push(timeAgoComponent);
|
||||
return acc;
|
||||
}, []).forEach(e => e.changeTime());
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
startTimeLoops();
|
||||
|
||||
const removeIntervals = () => clearInterval(this.timeLoopInterval);
|
||||
const startIntervals = () => startTimeLoops();
|
||||
|
||||
VueRealtimeListener(removeIntervals, startIntervals);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
export default (removeIntervals, startIntervals) => {
|
||||
window.removeEventListener('focus', startIntervals);
|
||||
window.removeEventListener('blur', removeIntervals);
|
||||
window.removeEventListener('onbeforeload', removeIntervals);
|
||||
|
||||
window.addEventListener('focus', startIntervals);
|
||||
window.addEventListener('blur', removeIntervals);
|
||||
window.addEventListener('onbeforeload', removeIntervals);
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
|
||||
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
|
||||
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
|
||||
|
@ -166,6 +165,32 @@ export default {
|
|||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Timeago components expects a number
|
||||
*
|
||||
* @return {type} description
|
||||
*/
|
||||
pipelineDuration() {
|
||||
if (this.pipeline.details && this.pipeline.details.duration) {
|
||||
return this.pipeline.details.duration;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Timeago component expects a String.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
pipelineFinishedAt() {
|
||||
if (this.pipeline.details && this.pipeline.details.finished_at) {
|
||||
return this.pipeline.details.finished_at;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
|
@ -192,7 +217,9 @@ export default {
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<time-ago :pipeline="pipeline"/>
|
||||
<time-ago
|
||||
:duration="pipelineDuration"
|
||||
:finished-time="pipelineFinishedAt" />
|
||||
|
||||
<td class="pipeline-actions">
|
||||
<div class="pull-right btn-group">
|
||||
|
|
|
@ -195,7 +195,6 @@
|
|||
border: 1px solid $dropdown-border-color;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 2px 4px $dropdown-shadow-color;
|
||||
overflow: hidden;
|
||||
@include set-invisible;
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
|
|
|
@ -104,6 +104,24 @@
|
|||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
.value {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.remove-token {
|
||||
display: inline-block;
|
||||
padding-left: 4px;
|
||||
padding-right: 8px;
|
||||
|
||||
.fa-close {
|
||||
color: $gl-text-color-disabled;
|
||||
}
|
||||
|
||||
&:hover .fa-close {
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
background-color: $filter-name-resting-color;
|
||||
color: $filter-name-text-color;
|
||||
|
@ -112,7 +130,7 @@
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.value {
|
||||
.value-container {
|
||||
background-color: $white-normal;
|
||||
color: $filter-value-text-color;
|
||||
border-radius: 0 2px 2px 0;
|
||||
|
@ -124,7 +142,7 @@
|
|||
background-color: $filter-name-selected-color;
|
||||
}
|
||||
|
||||
.value {
|
||||
.value-container {
|
||||
background-color: $filter-value-selected-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
// Ensure that image does not exceed viewport
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
|
|
|
@ -12,6 +12,13 @@
|
|||
max-width: $max_width;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mixin for markdown tables
|
||||
*/
|
||||
@mixin markdown-table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Base mixin for lists in GitLab
|
||||
*/
|
||||
|
|
|
@ -200,6 +200,7 @@
|
|||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
line-height: 1.8;
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
|
|
|
@ -101,11 +101,16 @@ ul.related-merge-requests > li {
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request-ci-status {
|
||||
.merge-request-ci-status,
|
||||
.related-merge-requests {
|
||||
.ci-status-link {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,10 @@ ul.notes {
|
|||
padding-left: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -614,6 +614,7 @@ pre.light-well {
|
|||
|
||||
.controls {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ci-status-link {
|
||||
|
|
|
@ -160,7 +160,6 @@
|
|||
|
||||
.tree-controls {
|
||||
float: right;
|
||||
margin-top: 11px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
|
|
|
@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list {
|
|||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wiki {
|
||||
table {
|
||||
@include markdown-table;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def respond_422
|
||||
head :unprocessable_entity
|
||||
end
|
||||
|
||||
def no_cache_headers
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
module MarkdownPreview
|
||||
private
|
||||
|
||||
def render_markdown_preview(text, markdown_context = {})
|
||||
render json: {
|
||||
body: view_context.markdown(text, markdown_context),
|
||||
references: {
|
||||
users: preview_referenced_users(text)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def preview_referenced_users(text)
|
||||
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
extractor.analyze(text, author: current_user)
|
||||
|
||||
extractor.users.map(&:username)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module RendersBlob
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def render_blob_json(blob)
|
||||
viewer =
|
||||
if params[:viewer] == 'rich'
|
||||
blob.rich_viewer
|
||||
else
|
||||
blob.simple_viewer
|
||||
end
|
||||
return render_404 unless viewer
|
||||
|
||||
render json: {
|
||||
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
class Projects::BlobController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
include CreatesCommit
|
||||
include RendersBlob
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
# Raised when given an invalid file path
|
||||
|
@ -34,8 +35,20 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
|
||||
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
|
||||
@blob.override_max_size! if params[:override_max_size] == 'true'
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
|
||||
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
|
||||
|
||||
render 'show'
|
||||
end
|
||||
|
||||
format.json do
|
||||
render_blob_json(@blob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
|
@ -96,7 +109,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
def blob
|
||||
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
|
||||
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project)
|
||||
|
||||
if @blob
|
||||
@blob
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class Projects::BuildsController < Projects::ApplicationController
|
||||
before_action :build, except: [:index, :cancel_all]
|
||||
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
|
||||
before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace]
|
||||
before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace]
|
||||
layout 'project'
|
||||
|
||||
|
@ -60,20 +60,22 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def retry
|
||||
return render_404 unless @build.retryable?
|
||||
return respond_422 unless @build.retryable?
|
||||
|
||||
build = Ci::Build.retry(@build, current_user)
|
||||
redirect_to build_path(build)
|
||||
end
|
||||
|
||||
def play
|
||||
return render_404 unless @build.playable?
|
||||
return respond_422 unless @build.playable?
|
||||
|
||||
build = @build.play(current_user)
|
||||
redirect_to build_path(build)
|
||||
end
|
||||
|
||||
def cancel
|
||||
return respond_422 unless @build.cancelable?
|
||||
|
||||
@build.cancel
|
||||
redirect_to build_path(@build)
|
||||
end
|
||||
|
@ -85,9 +87,12 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def erase
|
||||
@build.erase(erased_by: current_user)
|
||||
redirect_to namespace_project_build_path(project.namespace, project, @build),
|
||||
if @build.erase(erased_by: current_user)
|
||||
redirect_to namespace_project_build_path(project.namespace, project, @build),
|
||||
notice: "Build has been successfully erased!"
|
||||
else
|
||||
respond_422
|
||||
end
|
||||
end
|
||||
|
||||
def raw
|
||||
|
|
|
@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
|
|||
|
||||
return if cached_blob?
|
||||
|
||||
if @blob.lfs_pointer? && project.lfs_enabled?
|
||||
if @blob.valid_lfs_pointer?
|
||||
send_lfs_object
|
||||
else
|
||||
send_git_blob @repository, @blob
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Projects::WikisController < Projects::ApplicationController
|
||||
include MarkdownPreview
|
||||
|
||||
before_action :authorize_read_wiki!
|
||||
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
|
||||
before_action :authorize_admin_wiki!, only: :destroy
|
||||
|
@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def preview_markdown
|
||||
text = params[:text]
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
ext.analyze(text, author: current_user)
|
||||
|
||||
render json: {
|
||||
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
|
||||
references: {
|
||||
users: ext.users.map(&:username)
|
||||
}
|
||||
}
|
||||
def git_access
|
||||
end
|
||||
|
||||
def git_access
|
||||
def preview_markdown
|
||||
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
|
||||
|
||||
render_markdown_preview(params[:text], context)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
|
||||
# Call #wiki to make sure the Wiki Repo is initialized
|
||||
@project_wiki.wiki
|
||||
|
||||
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
|
||||
rescue ProjectWiki::CouldNotCreateWikiError
|
||||
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class ProjectsController < Projects::ApplicationController
|
||||
include IssuableCollections
|
||||
include ExtractsPath
|
||||
include MarkdownPreview
|
||||
|
||||
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
|
||||
before_action :project, except: [:index, :new, :create]
|
||||
|
@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def preview_markdown
|
||||
text = params[:text]
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
ext.analyze(text, author: current_user)
|
||||
|
||||
render json: {
|
||||
body: view_context.markdown(text),
|
||||
references: {
|
||||
users: ext.users.map(&:username)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def refs
|
||||
branches = BranchesFinder.new(@repository, params).execute.map(&:name)
|
||||
|
||||
|
@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController
|
|||
render json: options.to_json
|
||||
end
|
||||
|
||||
def preview_markdown
|
||||
render_markdown_preview(params[:text])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Render project landing depending of which features are available
|
||||
|
|
|
@ -2,6 +2,7 @@ class SnippetsController < ApplicationController
|
|||
include ToggleAwardEmoji
|
||||
include SpammableActions
|
||||
include SnippetsActions
|
||||
include MarkdownPreview
|
||||
|
||||
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
|
||||
|
||||
|
@ -77,6 +78,10 @@ class SnippetsController < ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def preview_markdown
|
||||
render_markdown_preview(params[:text], skip_project_check: true)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def snippet
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
# current_user - which user use
|
||||
# params:
|
||||
# scope: 'created-by-me' or 'assigned-to-me' or 'all'
|
||||
# state: 'open' or 'closed' or 'all'
|
||||
# state: 'open', 'closed', 'merged', or 'all'
|
||||
# group_id: integer
|
||||
# project_id: integer
|
||||
# milestone_title: string
|
||||
|
|
|
@ -196,38 +196,6 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def render_markup(file_name, file_content)
|
||||
if gitlab_markdown?(file_name)
|
||||
Hamlit::RailsHelpers.preserve(markdown(file_content))
|
||||
elsif asciidoc?(file_name)
|
||||
asciidoc(file_content)
|
||||
elsif plain?(file_name)
|
||||
content_tag :pre, class: 'plain-readme' do
|
||||
file_content
|
||||
end
|
||||
else
|
||||
other_markup(file_name, file_content)
|
||||
end
|
||||
rescue RuntimeError
|
||||
simple_format(file_content)
|
||||
end
|
||||
|
||||
def plain?(filename)
|
||||
Gitlab::MarkupHelper.plain?(filename)
|
||||
end
|
||||
|
||||
def markup?(filename)
|
||||
Gitlab::MarkupHelper.markup?(filename)
|
||||
end
|
||||
|
||||
def gitlab_markdown?(filename)
|
||||
Gitlab::MarkupHelper.gitlab_markdown?(filename)
|
||||
end
|
||||
|
||||
def asciidoc?(filename)
|
||||
Gitlab::MarkupHelper.asciidoc?(filename)
|
||||
end
|
||||
|
||||
def promo_host
|
||||
'about.gitlab.com'
|
||||
end
|
||||
|
|
|
@ -52,7 +52,7 @@ module BlobHelper
|
|||
|
||||
if !on_top_of_branch?(project, ref)
|
||||
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
||||
elsif blob.lfs_pointer?
|
||||
elsif blob.valid_lfs_pointer?
|
||||
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
||||
elsif can_modify_blob?(blob, project, ref)
|
||||
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
|
||||
|
@ -95,7 +95,7 @@ module BlobHelper
|
|||
end
|
||||
|
||||
def can_modify_blob?(blob, project = @project, ref = @ref)
|
||||
!blob.lfs_pointer? && can_edit_tree?(project, ref)
|
||||
!blob.valid_lfs_pointer? && can_edit_tree?(project, ref)
|
||||
end
|
||||
|
||||
def leave_edit_message
|
||||
|
@ -118,28 +118,15 @@ module BlobHelper
|
|||
icon("#{file_type_icon_class('file', mode, name)} fw")
|
||||
end
|
||||
|
||||
def blob_text_viewable?(blob)
|
||||
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
|
||||
end
|
||||
|
||||
def blob_rendered_as_text?(blob)
|
||||
blob_text_viewable?(blob) && blob.to_partial_path(@project) == 'text'
|
||||
end
|
||||
|
||||
def blob_size(blob)
|
||||
if blob.lfs_pointer?
|
||||
blob.lfs_size
|
||||
else
|
||||
blob.size
|
||||
end
|
||||
def blob_raw_url
|
||||
namespace_project_raw_path(@project.namespace, @project, @id)
|
||||
end
|
||||
|
||||
# SVGs can contain malicious JavaScript; only include whitelisted
|
||||
# elements and attributes. Note that this whitelist is by no means complete
|
||||
# and may omit some elements.
|
||||
def sanitize_svg(blob)
|
||||
blob.data = Gitlab::Sanitizers::SVG.clean(blob.data)
|
||||
blob
|
||||
def sanitize_svg_data(data)
|
||||
Gitlab::Sanitizers::SVG.clean(data)
|
||||
end
|
||||
|
||||
# If we blindly set the 'real' content type when serving a Git blob we
|
||||
|
@ -221,13 +208,42 @@ module BlobHelper
|
|||
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
|
||||
end
|
||||
|
||||
def copy_blob_content_button(blob)
|
||||
return if markup?(blob.name)
|
||||
|
||||
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
|
||||
def copy_blob_source_button(blob)
|
||||
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
|
||||
|
||||
def open_raw_file_button(path)
|
||||
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
|
||||
end
|
||||
|
||||
def blob_render_error_reason(viewer)
|
||||
case viewer.render_error
|
||||
when :too_large
|
||||
max_size =
|
||||
if viewer.absolutely_too_large?
|
||||
viewer.absolute_max_size
|
||||
elsif viewer.too_large?
|
||||
viewer.max_size
|
||||
end
|
||||
"it is larger than #{number_to_human_size(max_size)}"
|
||||
when :server_side_but_stored_in_lfs
|
||||
"it is stored in LFS"
|
||||
end
|
||||
end
|
||||
|
||||
def blob_render_error_options(viewer)
|
||||
options = []
|
||||
|
||||
if viewer.render_error == :too_large && viewer.can_override_max_size?
|
||||
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
|
||||
end
|
||||
|
||||
if viewer.rich? && viewer.blob.rendered_as_text?
|
||||
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
|
||||
end
|
||||
|
||||
options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
options
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
require 'nokogiri'
|
||||
|
||||
module GitlabMarkdownHelper
|
||||
module MarkupHelper
|
||||
def plain?(filename)
|
||||
Gitlab::MarkupHelper.plain?(filename)
|
||||
end
|
||||
|
||||
def markup?(filename)
|
||||
Gitlab::MarkupHelper.markup?(filename)
|
||||
end
|
||||
|
||||
def gitlab_markdown?(filename)
|
||||
Gitlab::MarkupHelper.gitlab_markdown?(filename)
|
||||
end
|
||||
|
||||
def asciidoc?(filename)
|
||||
Gitlab::MarkupHelper.asciidoc?(filename)
|
||||
end
|
||||
|
||||
# Use this in places where you would normally use link_to(gfm(...), ...).
|
||||
#
|
||||
# It solves a problem occurring with nested links (i.e.
|
||||
|
@ -11,7 +27,7 @@ module GitlabMarkdownHelper
|
|||
# explicitly produce the correct linking behavior (i.e.
|
||||
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
|
||||
def link_to_gfm(body, url, html_options = {})
|
||||
return "" if body.blank?
|
||||
return '' if body.blank?
|
||||
|
||||
context = {
|
||||
project: @project,
|
||||
|
@ -43,52 +59,6 @@ module GitlabMarkdownHelper
|
|||
fragment.to_html.html_safe
|
||||
end
|
||||
|
||||
def markdown(text, context = {})
|
||||
return "" unless text.present?
|
||||
|
||||
context[:project] ||= @project
|
||||
|
||||
html = Banzai.render(text, context)
|
||||
banzai_postprocess(html, context)
|
||||
end
|
||||
|
||||
def markdown_field(object, field)
|
||||
object = object.for_display if object.respond_to?(:for_display)
|
||||
return "" unless object.present?
|
||||
|
||||
html = Banzai.render_field(object, field)
|
||||
banzai_postprocess(html, object.banzai_render_context(field))
|
||||
end
|
||||
|
||||
def asciidoc(text)
|
||||
Gitlab::Asciidoc.render(
|
||||
text,
|
||||
project: @project,
|
||||
current_user: (current_user if defined?(current_user)),
|
||||
|
||||
# RelativeLinkFilter
|
||||
project_wiki: @project_wiki,
|
||||
requested_path: @path,
|
||||
ref: @ref,
|
||||
commit: @commit
|
||||
)
|
||||
end
|
||||
|
||||
def other_markup(file_name, text)
|
||||
Gitlab::OtherMarkup.render(
|
||||
file_name,
|
||||
text,
|
||||
project: @project,
|
||||
current_user: (current_user if defined?(current_user)),
|
||||
|
||||
# RelativeLinkFilter
|
||||
project_wiki: @project_wiki,
|
||||
requested_path: @path,
|
||||
ref: @ref,
|
||||
commit: @commit
|
||||
)
|
||||
end
|
||||
|
||||
# Return the first line of +text+, up to +max_chars+, after parsing the line
|
||||
# as Markdown. HTML tags in the parsed output are not counted toward the
|
||||
# +max_chars+ limit. If the length limit falls within a tag's contents, then
|
||||
|
@ -99,15 +69,63 @@ module GitlabMarkdownHelper
|
|||
truncate_visible(md, max_chars || md.length) if md.present?
|
||||
end
|
||||
|
||||
def markdown(text, context = {})
|
||||
return '' unless text.present?
|
||||
|
||||
context[:project] ||= @project
|
||||
html = markdown_unsafe(text, context)
|
||||
banzai_postprocess(html, context)
|
||||
end
|
||||
|
||||
def markdown_field(object, field)
|
||||
object = object.for_display if object.respond_to?(:for_display)
|
||||
return '' unless object.present?
|
||||
|
||||
html = Banzai.render_field(object, field)
|
||||
banzai_postprocess(html, object.banzai_render_context(field))
|
||||
end
|
||||
|
||||
def markup(file_name, text, context = {})
|
||||
context[:project] ||= @project
|
||||
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
|
||||
banzai_postprocess(html, context)
|
||||
end
|
||||
|
||||
def render_wiki_content(wiki_page)
|
||||
case wiki_page.format
|
||||
when :markdown
|
||||
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
|
||||
when :asciidoc
|
||||
asciidoc(wiki_page.content)
|
||||
text = wiki_page.content
|
||||
return '' unless text.present?
|
||||
|
||||
context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
|
||||
|
||||
html =
|
||||
case wiki_page.format
|
||||
when :markdown
|
||||
markdown_unsafe(text, context)
|
||||
when :asciidoc
|
||||
asciidoc_unsafe(text)
|
||||
else
|
||||
wiki_page.formatted_content.html_safe
|
||||
end
|
||||
|
||||
banzai_postprocess(html, context)
|
||||
end
|
||||
|
||||
def markup_unsafe(file_name, text, context = {})
|
||||
return '' unless text.present?
|
||||
|
||||
if gitlab_markdown?(file_name)
|
||||
Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
|
||||
elsif asciidoc?(file_name)
|
||||
asciidoc_unsafe(text)
|
||||
elsif plain?(file_name)
|
||||
content_tag :pre, class: 'plain-readme' do
|
||||
text
|
||||
end
|
||||
else
|
||||
wiki_page.formatted_content.html_safe
|
||||
other_markup_unsafe(file_name, text)
|
||||
end
|
||||
rescue RuntimeError
|
||||
simple_format(text)
|
||||
end
|
||||
|
||||
# Returns the text necessary to reference `entity` across projects
|
||||
|
@ -183,10 +201,10 @@ module GitlabMarkdownHelper
|
|||
end
|
||||
|
||||
def markdown_toolbar_button(options = {})
|
||||
data = options[:data].merge({ container: "body" })
|
||||
data = options[:data].merge({ container: 'body' })
|
||||
content_tag :button,
|
||||
type: "button",
|
||||
class: "toolbar-btn js-md has-tooltip hidden-xs",
|
||||
type: 'button',
|
||||
class: 'toolbar-btn js-md has-tooltip hidden-xs',
|
||||
tabindex: -1,
|
||||
data: data,
|
||||
title: options[:title],
|
||||
|
@ -195,17 +213,34 @@ module GitlabMarkdownHelper
|
|||
end
|
||||
end
|
||||
|
||||
def markdown_unsafe(text, context = {})
|
||||
Banzai.render(text, context)
|
||||
end
|
||||
|
||||
def asciidoc_unsafe(text)
|
||||
Gitlab::Asciidoc.render(text)
|
||||
end
|
||||
|
||||
def other_markup_unsafe(file_name, text)
|
||||
Gitlab::OtherMarkup.render(file_name, text)
|
||||
end
|
||||
|
||||
# Calls Banzai.post_process with some common context options
|
||||
def banzai_postprocess(html, context)
|
||||
def banzai_postprocess(html, context = {})
|
||||
return '' unless html.present?
|
||||
|
||||
context.merge!(
|
||||
current_user: (current_user if defined?(current_user)),
|
||||
|
||||
# RelativeLinkFilter
|
||||
requested_path: @path,
|
||||
commit: @commit,
|
||||
project_wiki: @project_wiki,
|
||||
ref: @ref
|
||||
ref: @ref,
|
||||
requested_path: @path
|
||||
)
|
||||
|
||||
Banzai.post_process(html, context)
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
|
@ -160,12 +160,17 @@ module ProjectsHelper
|
|||
end
|
||||
|
||||
def project_list_cache_key(project)
|
||||
key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3']
|
||||
key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
|
||||
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
|
||||
|
||||
key
|
||||
end
|
||||
|
||||
def load_pipeline_status(projects)
|
||||
Gitlab::Cache::Ci::ProjectPipelineStatus.
|
||||
load_in_batch_for_projects(projects)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def repo_children_classes(field)
|
||||
|
|
|
@ -13,8 +13,8 @@ module ServicesHelper
|
|||
"Event will be triggered when a confidential issue is created/updated/closed"
|
||||
when "merge_request", "merge_request_events"
|
||||
"Event will be triggered when a merge request is created/updated/merged"
|
||||
when "build", "build_events"
|
||||
"Event will be triggered when a build status changes"
|
||||
when "pipeline", "pipeline_events"
|
||||
"Event will be triggered when a pipeline status changes"
|
||||
when "wiki_page", "wiki_page_events"
|
||||
"Event will be triggered when a wiki page is created/updated"
|
||||
when "commit", "commit_events"
|
||||
|
|
|
@ -12,10 +12,6 @@ module TreeHelper
|
|||
tree.html_safe
|
||||
end
|
||||
|
||||
def render_readme(readme)
|
||||
render_markup(readme.name, readme.data)
|
||||
end
|
||||
|
||||
# Return an image icon depending on the file type and mode
|
||||
#
|
||||
# type - String type of the tree item; either 'folder' or 'file'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class BaseMailer < ActionMailer::Base
|
||||
helper ApplicationHelper
|
||||
helper GitlabMarkdownHelper
|
||||
helper MarkupHelper
|
||||
|
||||
attr_accessor :current_user
|
||||
helper_method :current_user, :can?
|
||||
|
|
|
@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
|
||||
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
|
||||
|
||||
validates :uuid, presence: true
|
||||
|
||||
validates :session_expire_delay,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
@ -163,6 +165,7 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
before_validation :ensure_uuid!
|
||||
before_save :ensure_runners_registration_token
|
||||
before_save :ensure_health_check_access_token
|
||||
|
||||
|
@ -348,6 +351,12 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
|
||||
private
|
||||
|
||||
def ensure_uuid!
|
||||
return if uuid?
|
||||
|
||||
self.uuid = SecureRandom.uuid
|
||||
end
|
||||
|
||||
def check_repository_storages
|
||||
invalid = repository_storages - Gitlab.config.repositories.storages.keys
|
||||
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
|
||||
|
|
|
@ -3,8 +3,40 @@ class Blob < SimpleDelegator
|
|||
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
|
||||
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
|
||||
|
||||
# The maximum size of an SVG that can be displayed.
|
||||
MAXIMUM_SVG_SIZE = 2.megabytes
|
||||
MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
|
||||
|
||||
# Finding a viewer for a blob happens based only on extension and whether the
|
||||
# blob is binary or text, which means 1 blob should only be matched by 1 viewer,
|
||||
# and the order of these viewers doesn't really matter.
|
||||
#
|
||||
# However, when the blob is an LFS pointer, we cannot know for sure whether the
|
||||
# file being pointed to is binary or text. In this case, we match only on
|
||||
# extension, preferring binary viewers over text ones if both exist, since the
|
||||
# large files referred to in "Large File Storage" are much more likely to be
|
||||
# binary than text.
|
||||
#
|
||||
# `.stl` files, for example, exist in both binary and text forms, and are
|
||||
# handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
|
||||
# type. LFS pointers to `.stl` files are assumed to always be the binary kind,
|
||||
# and use the `BinarySTL` viewer.
|
||||
RICH_VIEWERS = [
|
||||
BlobViewer::Markup,
|
||||
BlobViewer::Notebook,
|
||||
BlobViewer::SVG,
|
||||
|
||||
BlobViewer::Image,
|
||||
BlobViewer::Sketch,
|
||||
|
||||
BlobViewer::PDF,
|
||||
|
||||
BlobViewer::BinarySTL,
|
||||
BlobViewer::TextSTL,
|
||||
].freeze
|
||||
|
||||
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
|
||||
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
|
||||
|
||||
attr_reader :project
|
||||
|
||||
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
|
||||
#
|
||||
|
@ -16,10 +48,16 @@ class Blob < SimpleDelegator
|
|||
#
|
||||
# blob = Blob.decorate(nil)
|
||||
# puts "truthy" if blob # No output
|
||||
def self.decorate(blob)
|
||||
def self.decorate(blob, project = nil)
|
||||
return if blob.nil?
|
||||
|
||||
new(blob)
|
||||
new(blob, project)
|
||||
end
|
||||
|
||||
def initialize(blob, project = nil)
|
||||
@project = project
|
||||
|
||||
super(blob)
|
||||
end
|
||||
|
||||
# Returns the data of the blob.
|
||||
|
@ -35,82 +73,107 @@ class Blob < SimpleDelegator
|
|||
end
|
||||
|
||||
def no_highlighting?
|
||||
size && size > 1.megabyte
|
||||
size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
|
||||
end
|
||||
|
||||
def only_display_raw?
|
||||
def too_large?
|
||||
size && truncated?
|
||||
end
|
||||
|
||||
# Returns the size of the file that this blob represents. If this blob is an
|
||||
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
|
||||
# the size of the blob itself.
|
||||
def raw_size
|
||||
if valid_lfs_pointer?
|
||||
lfs_size
|
||||
else
|
||||
size
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether the file that this blob represents is binary. If this blob is
|
||||
# an LFS pointer, we assume the file stored in LFS is binary, unless a
|
||||
# text-based rich blob viewer matched on the file's extension. Otherwise, this
|
||||
# depends on the type of the blob itself.
|
||||
def raw_binary?
|
||||
if valid_lfs_pointer?
|
||||
if rich_viewer
|
||||
rich_viewer.binary?
|
||||
else
|
||||
true
|
||||
end
|
||||
else
|
||||
binary?
|
||||
end
|
||||
end
|
||||
|
||||
def extension
|
||||
extname.downcase.delete('.')
|
||||
end
|
||||
|
||||
def svg?
|
||||
text? && language && language.name == 'SVG'
|
||||
end
|
||||
|
||||
def pdf?
|
||||
extension == 'pdf'
|
||||
end
|
||||
|
||||
def ipython_notebook?
|
||||
text? && language&.name == 'Jupyter Notebook'
|
||||
end
|
||||
|
||||
def sketch?
|
||||
binary? && extension == 'sketch'
|
||||
end
|
||||
|
||||
def stl?
|
||||
extension == 'stl'
|
||||
end
|
||||
|
||||
def markup?
|
||||
text? && Gitlab::MarkupHelper.markup?(name)
|
||||
end
|
||||
|
||||
def size_within_svg_limits?
|
||||
size <= MAXIMUM_SVG_SIZE
|
||||
@extension ||= extname.downcase.delete('.')
|
||||
end
|
||||
|
||||
def video?
|
||||
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
|
||||
UploaderHelper::VIDEO_EXT.include?(extension)
|
||||
end
|
||||
|
||||
def to_partial_path(project)
|
||||
if lfs_pointer?
|
||||
if project.lfs_enabled?
|
||||
'download'
|
||||
else
|
||||
'text'
|
||||
end
|
||||
elsif image?
|
||||
'image'
|
||||
elsif svg?
|
||||
'svg'
|
||||
elsif pdf?
|
||||
'pdf'
|
||||
elsif ipython_notebook?
|
||||
'notebook'
|
||||
elsif sketch?
|
||||
'sketch'
|
||||
elsif stl?
|
||||
'stl'
|
||||
elsif markup?
|
||||
if only_display_raw?
|
||||
'too_large'
|
||||
else
|
||||
'markup'
|
||||
end
|
||||
elsif text?
|
||||
if only_display_raw?
|
||||
'too_large'
|
||||
else
|
||||
'text'
|
||||
end
|
||||
else
|
||||
'download'
|
||||
def readable_text?
|
||||
text? && !valid_lfs_pointer? && !too_large?
|
||||
end
|
||||
|
||||
def valid_lfs_pointer?
|
||||
lfs_pointer? && project&.lfs_enabled?
|
||||
end
|
||||
|
||||
def invalid_lfs_pointer?
|
||||
lfs_pointer? && !project&.lfs_enabled?
|
||||
end
|
||||
|
||||
def simple_viewer
|
||||
@simple_viewer ||= simple_viewer_class.new(self)
|
||||
end
|
||||
|
||||
def rich_viewer
|
||||
return @rich_viewer if defined?(@rich_viewer)
|
||||
|
||||
@rich_viewer = rich_viewer_class&.new(self)
|
||||
end
|
||||
|
||||
def rendered_as_text?(ignore_errors: true)
|
||||
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
|
||||
end
|
||||
|
||||
def show_viewer_switcher?
|
||||
rendered_as_text? && rich_viewer
|
||||
end
|
||||
|
||||
def override_max_size!
|
||||
simple_viewer&.override_max_size = true
|
||||
rich_viewer&.override_max_size = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def simple_viewer_class
|
||||
if empty?
|
||||
BlobViewer::Empty
|
||||
elsif raw_binary?
|
||||
BlobViewer::Download
|
||||
else # text
|
||||
BlobViewer::Text
|
||||
end
|
||||
end
|
||||
|
||||
def rich_viewer_class
|
||||
return if invalid_lfs_pointer? || empty?
|
||||
|
||||
classes =
|
||||
if valid_lfs_pointer?
|
||||
BINARY_VIEWERS + TEXT_VIEWERS
|
||||
elsif binary?
|
||||
BINARY_VIEWERS
|
||||
else # text
|
||||
TEXT_VIEWERS
|
||||
end
|
||||
|
||||
classes.find { |viewer_class| viewer_class.can_render?(self) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
module BlobViewer
|
||||
class Base
|
||||
class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
|
||||
|
||||
delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
|
||||
|
||||
attr_reader :blob
|
||||
attr_accessor :override_max_size
|
||||
|
||||
def initialize(blob)
|
||||
@blob = blob
|
||||
end
|
||||
|
||||
def self.partial_path
|
||||
"projects/blob/viewers/#{partial_name}"
|
||||
end
|
||||
|
||||
def self.rich?
|
||||
type == :rich
|
||||
end
|
||||
|
||||
def self.simple?
|
||||
type == :simple
|
||||
end
|
||||
|
||||
def self.client_side?
|
||||
client_side
|
||||
end
|
||||
|
||||
def self.server_side?
|
||||
!client_side?
|
||||
end
|
||||
|
||||
def self.binary?
|
||||
binary
|
||||
end
|
||||
|
||||
def self.text?
|
||||
!binary?
|
||||
end
|
||||
|
||||
def self.can_render?(blob)
|
||||
!extensions || extensions.include?(blob.extension)
|
||||
end
|
||||
|
||||
def too_large?
|
||||
blob.raw_size > max_size
|
||||
end
|
||||
|
||||
def absolutely_too_large?
|
||||
blob.raw_size > absolute_max_size
|
||||
end
|
||||
|
||||
def can_override_max_size?
|
||||
too_large? && !absolutely_too_large?
|
||||
end
|
||||
|
||||
# This method is used on the server side to check whether we can attempt to
|
||||
# render the blob at all. Human-readable error messages are found in the
|
||||
# `BlobHelper#blob_render_error_reason` helper.
|
||||
#
|
||||
# This method does not and should not load the entire blob contents into
|
||||
# memory, and should not be overridden to do so in order to validate the
|
||||
# format of the blob.
|
||||
#
|
||||
# Prefer to implement a client-side viewer, where the JS component loads the
|
||||
# binary from `blob_raw_url` and does its own format validation and error
|
||||
# rendering, especially for potentially large binary formats.
|
||||
def render_error
|
||||
return @render_error if defined?(@render_error)
|
||||
|
||||
@render_error =
|
||||
if server_side_but_stored_in_lfs?
|
||||
# Files stored in LFS can only be rendered using a client-side viewer,
|
||||
# since we do not want to read large amounts of data into memory on the
|
||||
# server side. Client-side viewers use JS and can fetch the file from
|
||||
# `blob_raw_url` using AJAX.
|
||||
:server_side_but_stored_in_lfs
|
||||
elsif override_max_size ? absolutely_too_large? : too_large?
|
||||
:too_large
|
||||
end
|
||||
end
|
||||
|
||||
def prepare!
|
||||
if server_side? && blob.project
|
||||
blob.load_all_data!(blob.project.repository)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def server_side_but_stored_in_lfs?
|
||||
server_side? && blob.valid_lfs_pointer?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
module BlobViewer
|
||||
class BinarySTL < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'stl'
|
||||
self.extensions = %w(stl)
|
||||
self.binary = true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module ClientSide
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.client_side = true
|
||||
self.max_size = 10.megabytes
|
||||
self.absolute_max_size = 50.megabytes
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module BlobViewer
|
||||
class Download < Base
|
||||
include Simple
|
||||
# We treat the Download viewer as if it renders the content client-side,
|
||||
# so that it doesn't attempt to load the entire blob contents and is
|
||||
# rendered synchronously instead of loaded asynchronously.
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'download'
|
||||
self.binary = true
|
||||
|
||||
# We can always render the Download viewer, even if the blob is in LFS or too large.
|
||||
def render_error
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
module BlobViewer
|
||||
class Empty < Base
|
||||
include Simple
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'empty'
|
||||
self.binary = true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Image < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'image'
|
||||
self.extensions = UploaderHelper::IMAGE_EXT
|
||||
self.binary = true
|
||||
self.switcher_icon = 'picture-o'
|
||||
self.switcher_title = 'image'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
module BlobViewer
|
||||
class Markup < Base
|
||||
include Rich
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'markup'
|
||||
self.extensions = Gitlab::MarkupHelper::EXTENSIONS
|
||||
self.binary = false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Notebook < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'notebook'
|
||||
self.extensions = %w(ipynb)
|
||||
self.binary = false
|
||||
self.switcher_icon = 'file-text-o'
|
||||
self.switcher_title = 'notebook'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class PDF < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'pdf'
|
||||
self.extensions = %w(pdf)
|
||||
self.binary = true
|
||||
self.switcher_icon = 'file-pdf-o'
|
||||
self.switcher_title = 'PDF'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module Rich
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.type = :rich
|
||||
self.switcher_icon = 'file-text-o'
|
||||
self.switcher_title = 'rendered file'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module ServerSide
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.client_side = false
|
||||
self.max_size = 2.megabytes
|
||||
self.absolute_max_size = 5.megabytes
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
module Simple
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.type = :simple
|
||||
self.switcher_icon = 'code'
|
||||
self.switcher_title = 'source'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class Sketch < Base
|
||||
include Rich
|
||||
include ClientSide
|
||||
|
||||
self.partial_name = 'sketch'
|
||||
self.extensions = %w(sketch)
|
||||
self.binary = true
|
||||
self.switcher_icon = 'file-image-o'
|
||||
self.switcher_title = 'preview'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module BlobViewer
|
||||
class SVG < Base
|
||||
include Rich
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'svg'
|
||||
self.extensions = %w(svg)
|
||||
self.binary = false
|
||||
self.switcher_icon = 'picture-o'
|
||||
self.switcher_title = 'image'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
module BlobViewer
|
||||
class Text < Base
|
||||
include Simple
|
||||
include ServerSide
|
||||
|
||||
self.partial_name = 'text'
|
||||
self.binary = false
|
||||
self.max_size = 1.megabyte
|
||||
self.absolute_max_size = 10.megabytes
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module BlobViewer
|
||||
class TextSTL < BinarySTL
|
||||
self.binary = false
|
||||
end
|
||||
end
|
|
@ -316,7 +316,7 @@ class Commit
|
|||
def uri_type(path)
|
||||
entry = @raw.tree.path(path)
|
||||
if entry[:type] == :blob
|
||||
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
|
||||
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
|
||||
blob.image? || blob.video? ? :raw : :blob
|
||||
else
|
||||
entry[:type]
|
||||
|
|
|
@ -120,7 +120,9 @@ module CacheMarkdownField
|
|||
attrs
|
||||
end
|
||||
|
||||
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
|
||||
# Using before_update here conflicts with elasticsearch-model somehow
|
||||
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
|
||||
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
|
|
|
@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
|
|||
|
||||
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
|
||||
|
||||
delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
|
||||
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
|
||||
delegate :title, to: :issue, prefix: true, allow_nil: true
|
||||
delegate :title, to: :merge_request, prefix: true, allow_nil: true
|
||||
delegate :title, to: :note, prefix: true, allow_nil: true
|
||||
|
|
|
@ -34,6 +34,7 @@ class Label < ActiveRecord::Base
|
|||
|
||||
scope :templates, -> { where(template: true) }
|
||||
scope :with_title, ->(title) { where(title: title) }
|
||||
scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
|
||||
|
||||
def self.prioritized(project)
|
||||
joins(:priorities)
|
||||
|
|
|
@ -107,7 +107,8 @@ module Network
|
|||
def find_commits(skip = 0)
|
||||
opts = {
|
||||
max_count: self.class.max_count,
|
||||
skip: skip
|
||||
skip: skip,
|
||||
order: :date
|
||||
}
|
||||
|
||||
opts[:ref] = @commit.id if @filter_ref
|
||||
|
|
|
@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
|
|||
|
||||
attr_accessor :new_default_branch
|
||||
attr_accessor :old_path_with_namespace
|
||||
attr_writer :pipeline_status
|
||||
|
||||
alias_attribute :title, :name
|
||||
|
||||
|
@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
# Lazy loading of the `pipeline_status` attribute
|
||||
def pipeline_status
|
||||
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ class ChatNotificationService < Service
|
|||
end
|
||||
|
||||
def can_test?
|
||||
super && valid?
|
||||
valid?
|
||||
end
|
||||
|
||||
def self.supported_events
|
||||
|
|
|
@ -17,9 +17,9 @@ class Repository
|
|||
# same name. The cache key used by those methods must also match method's
|
||||
# name.
|
||||
#
|
||||
# For example, for entry `:readme` there's a method called `readme` which
|
||||
# stores its data in the `readme` cache key.
|
||||
CACHED_METHODS = %i(size commit_count readme contribution_guide
|
||||
# For example, for entry `:commit_count` there's a method called `commit_count` which
|
||||
# stores its data in the `commit_count` cache key.
|
||||
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
|
||||
changelog license_blob license_key gitignore koding_yml
|
||||
gitlab_ci_yml branch_names tag_names branch_count
|
||||
tag_count avatar exists? empty? root_ref).freeze
|
||||
|
@ -28,7 +28,7 @@ class Repository
|
|||
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
|
||||
# the corresponding methods to call for refreshing caches.
|
||||
METHOD_CACHES_FOR_FILE_TYPES = {
|
||||
readme: :readme,
|
||||
readme: :rendered_readme,
|
||||
changelog: :changelog,
|
||||
license: %i(license_blob license_key),
|
||||
contributing: :contribution_guide,
|
||||
|
@ -450,7 +450,7 @@ class Repository
|
|||
|
||||
def blob_at(sha, path)
|
||||
unless Gitlab::Git.blank_ref?(sha)
|
||||
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
|
||||
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
|
||||
end
|
||||
rescue Gitlab::Git::Repository::NoRepository
|
||||
nil
|
||||
|
@ -527,7 +527,11 @@ class Repository
|
|||
head.readme
|
||||
end
|
||||
end
|
||||
cache_method :readme
|
||||
|
||||
def rendered_readme
|
||||
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
|
||||
end
|
||||
cache_method :rendered_readme
|
||||
|
||||
def contribution_guide
|
||||
file_on_head(:contributing)
|
||||
|
|
|
@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
|
|||
has_one :service_hook
|
||||
|
||||
validates :project_id, presence: true, unless: proc { |service| service.template? }
|
||||
validates :type, presence: true
|
||||
|
||||
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
|
||||
scope :issue_trackers, -> { where(category: 'issue_tracker') }
|
||||
|
@ -131,7 +132,7 @@ class Service < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def can_test?
|
||||
!project.empty_repo?
|
||||
true
|
||||
end
|
||||
|
||||
# reason why service cannot be tested
|
||||
|
|
|
@ -1068,11 +1068,13 @@ class User < ActiveRecord::Base
|
|||
User.find_by_email(s)
|
||||
end
|
||||
|
||||
scope.create(
|
||||
user = scope.build(
|
||||
username: username,
|
||||
email: email,
|
||||
&creation_block
|
||||
)
|
||||
user.save(validate: false)
|
||||
user
|
||||
ensure
|
||||
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
|
||||
end
|
||||
|
|
|
@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy
|
|||
def rules
|
||||
team_access!(user)
|
||||
|
||||
owner = project.owner == user ||
|
||||
(project.group && project.group.has_owner?(user))
|
||||
|
||||
owner_access! if user.admin? || owner
|
||||
team_member_owner_access! if owner
|
||||
owner_access! if user.admin? || owner?
|
||||
team_member_owner_access! if owner?
|
||||
|
||||
if project.public? || (project.internal? && !user.external?)
|
||||
guest_access!
|
||||
public_access!
|
||||
|
||||
if project.request_access_enabled &&
|
||||
!(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
|
||||
can! :request_access
|
||||
end
|
||||
can! :request_access if access_requestable?
|
||||
end
|
||||
|
||||
archived_access! if project.archived?
|
||||
|
@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy
|
|||
@subject
|
||||
end
|
||||
|
||||
def owner?
|
||||
return @owner if defined?(@owner)
|
||||
|
||||
@owner = project.owner == user ||
|
||||
(project.group && project.group.has_owner?(user))
|
||||
end
|
||||
|
||||
def guest_access!
|
||||
can! :read_project
|
||||
can! :read_board
|
||||
|
@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy
|
|||
disabled_features!
|
||||
end
|
||||
|
||||
def project_group_member?(user)
|
||||
project.group &&
|
||||
(
|
||||
project.group.members_with_parents.exists?(user_id: user.id) ||
|
||||
project.group.requesters.exists?(user_id: user.id)
|
||||
)
|
||||
end
|
||||
|
||||
def block_issues_abilities
|
||||
unless project.feature_available?(:issues, user)
|
||||
cannot! :read_issue if project.default_issues_tracker?
|
||||
|
@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
private
|
||||
|
||||
def project_group_member?(user)
|
||||
project.group &&
|
||||
(
|
||||
project.group.members_with_parents.exists?(user_id: user.id) ||
|
||||
project.group.requesters.exists?(user_id: user.id)
|
||||
)
|
||||
end
|
||||
|
||||
def access_requestable?
|
||||
project.request_access_enabled &&
|
||||
!owner? &&
|
||||
!user.admin? &&
|
||||
!project.team.member?(user) &&
|
||||
!project_group_member?(user)
|
||||
end
|
||||
|
||||
# A base set of abilities for read-only users, which
|
||||
# is then augmented as necessary for anonymous and other
|
||||
# read-only users.
|
||||
|
|
|
@ -61,7 +61,7 @@ module Boards
|
|||
if moving_to_list.movable?
|
||||
moving_from_list.label_id
|
||||
else
|
||||
project.boards.joins(:lists).merge(List.movable).pluck(:label_id)
|
||||
Label.on_project_boards(project.id).pluck(:label_id)
|
||||
end
|
||||
|
||||
Array(label_ids).compact
|
||||
|
|
|
@ -97,7 +97,8 @@ module Projects
|
|||
system_hook_service.execute_hooks_for(@project, :create)
|
||||
|
||||
unless @project.group || @project.gitlab_project_import?
|
||||
@project.team << [current_user, :master, current_user]
|
||||
owners = [current_user, @project.namespace.owner].compact.uniq
|
||||
@project.add_master(owners, current_user: current_user)
|
||||
end
|
||||
|
||||
@project.group&.refresh_members_authorized_projects
|
||||
|
|
|
@ -330,6 +330,28 @@ module SlashCommands
|
|||
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
|
||||
end
|
||||
|
||||
desc 'Move issue from one column of the board to another'
|
||||
params '~"Target column"'
|
||||
condition do
|
||||
issuable.is_a?(Issue) &&
|
||||
current_user.can?(:"update_#{issuable.to_ability_name}", issuable) &&
|
||||
issuable.project.boards.count == 1
|
||||
end
|
||||
command :board_move do |target_list_name|
|
||||
label_ids = find_label_ids(target_list_name)
|
||||
|
||||
if label_ids.size == 1
|
||||
label_id = label_ids.first
|
||||
|
||||
# Ensure this label corresponds to a list on the board
|
||||
next unless Label.on_project_boards(issuable.project_id).where(id: label_id).exists?
|
||||
|
||||
@updates[:remove_label_ids] =
|
||||
issuable.labels.on_project_boards(issuable.project_id).where.not(id: label_id).pluck(:id)
|
||||
@updates[:add_label_ids] = [label_id]
|
||||
end
|
||||
end
|
||||
|
||||
def find_label_ids(labels_param)
|
||||
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
|
||||
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
|
||||
|
|
|
@ -8,6 +8,7 @@ xml.entry do
|
|||
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
|
||||
|
||||
xml.author do
|
||||
xml.username event.author_username
|
||||
xml.name event.author_name
|
||||
xml.email event.author_public_email
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
- else
|
||||
%hr
|
||||
- blob = diff_file.blob
|
||||
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
|
||||
- if blob && blob.readable_text?
|
||||
%table.code.white
|
||||
= render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
|
||||
- else
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
- if can?(current_user, :push_code, @project)
|
||||
= link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
|
||||
.file-content.wiki
|
||||
= cache(readme_cache_key) do
|
||||
= render_readme(readme)
|
||||
= markup(readme.name, readme.data, rendered: @repository.rendered_readme)
|
||||
- else
|
||||
.row-content-block.second-block.center
|
||||
%h3.page-title
|
||||
|
|
|
@ -26,9 +26,4 @@
|
|||
%article.file-holder
|
||||
= render "projects/blob/header", blob: blob
|
||||
|
||||
- if blob.empty?
|
||||
.file-content.code
|
||||
.nothing-here-block
|
||||
Empty file
|
||||
- else
|
||||
= render blob.to_partial_path(@project), blob: blob
|
||||
= render 'projects/blob/content', blob: blob
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
- simple_viewer = blob.simple_viewer
|
||||
- rich_viewer = blob.rich_viewer
|
||||
- rich_viewer_active = rich_viewer && params[:viewer] != 'simple'
|
||||
|
||||
= render 'projects/blob/viewer', viewer: simple_viewer, hidden: rich_viewer_active
|
||||
|
||||
- if rich_viewer
|
||||
= render 'projects/blob/viewer', viewer: rich_viewer, hidden: !rich_viewer_active
|
|
@ -1,7 +0,0 @@
|
|||
.file-content.blob_file.blob-no-preview
|
||||
.center
|
||||
= link_to namespace_project_raw_path(@project.namespace, @project, @id) do
|
||||
%h1.light
|
||||
%i.fa.fa-download
|
||||
%h4
|
||||
Download (#{number_to_human_size blob_size(blob)})
|
|
@ -9,17 +9,19 @@
|
|||
= copy_file_path_button(blob.path)
|
||||
|
||||
%small
|
||||
= number_to_human_size(blob_size(blob))
|
||||
= number_to_human_size(blob.raw_size)
|
||||
|
||||
.file-actions.hidden-xs
|
||||
= render 'projects/blob/viewer_switcher', blob: blob unless blame
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
= copy_blob_content_button(blob) if !blame && blob_rendered_as_text?(blob)
|
||||
= copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false)
|
||||
= open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
|
||||
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
-# only show normal/blame view links for text files
|
||||
- if blob_text_viewable?(blob)
|
||||
- if blob.readable_text?
|
||||
- if blame
|
||||
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
|
||||
class: 'btn btn-sm'
|
||||
|
@ -34,7 +36,7 @@
|
|||
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
|
||||
|
||||
.btn-group{ role: "group" }<
|
||||
= edit_blob_link if blob_text_viewable?(blob)
|
||||
= edit_blob_link if blob.readable_text?
|
||||
- if current_user
|
||||
= replace_blob_link
|
||||
= delete_blob_link
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
.file-content.image_file
|
||||
%img{ src: namespace_project_raw_path(@project.namespace, @project, @id), alt: blob.name }
|
|
@ -1,4 +1,4 @@
|
|||
- blob.load_all_data!(@repository)
|
||||
|
||||
.file-content.wiki
|
||||
= render_markup(blob.name, blob.data)
|
||||
= markup(blob.name, blob.data)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.file-content.code
|
||||
.nothing-here-block
|
||||
The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
|
||||
|
||||
You can
|
||||
= blob_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
|
||||
instead.
|
|
@ -1,9 +0,0 @@
|
|||
- if blob.size_within_svg_limits?
|
||||
-# We need to scrub SVG but we cannot do so in the RawController: it would
|
||||
-# be wrong/strange if RawController modified the data.
|
||||
- blob.load_all_data!(@repository)
|
||||
- blob = sanitize_svg(blob)
|
||||
.file-content.image_file
|
||||
%img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: blob.name }
|
||||
- else
|
||||
= render 'too_large'
|
|
@ -1,2 +0,0 @@
|
|||
- blob.load_all_data!(@repository)
|
||||
= render 'shared/file_highlight', blob: blob, repository: @repository
|
|
@ -1,5 +0,0 @@
|
|||
.file-content.code
|
||||
.nothing-here-block
|
||||
The file could not be displayed as it is too large, you can
|
||||
#{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
|
||||
instead.
|
|
@ -0,0 +1,14 @@
|
|||
- hidden = local_assigns.fetch(:hidden, false)
|
||||
- render_error = viewer.render_error
|
||||
- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
|
||||
|
||||
- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
|
||||
.blob-viewer{ data: { type: viewer.type, url: url }, class: ('hidden' if hidden) }
|
||||
- if load_asynchronously
|
||||
.text-center.prepend-top-default.append-bottom-default
|
||||
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
|
||||
- elsif render_error
|
||||
= render 'projects/blob/render_error', viewer: viewer
|
||||
- else
|
||||
- viewer.prepare!
|
||||
= render viewer.partial_path, viewer: viewer
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue