Commit 5004579b authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into mia_backort[ci skip]

parents 79b8323d 10c1bf2d
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
/vendor/ /vendor/
karma.config.js karma.config.js
webpack.config.js webpack.config.js
/app/assets/javascripts/locale/**/*.js
*.log *.log
*.swp *.swp
*.mo
*.edit.po
.DS_Store .DS_Store
.bundle .bundle
.chef .chef
...@@ -54,3 +56,4 @@ eslint-report.html ...@@ -54,3 +56,4 @@ eslint-report.html
/shared/* /shared/*
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES
...@@ -23,6 +23,7 @@ before_script: ...@@ -23,6 +23,7 @@ before_script:
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
stages: stages:
- build
- prepare - prepare
- test - test
- post-test - post-test
...@@ -137,6 +138,28 @@ stages: ...@@ -137,6 +138,28 @@ stages:
<<: *only-master-and-ee-or-mysql <<: *only-master-and-ee-or-mysql
<<: *except-docs <<: *except-docs
# Trigger a package build on omnibus-gitlab repository
build-package:
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
stage: build
when: manual
script:
# If no branch in omnibus is specified, trigger pipeline against master
- if [ -z "$OMNIBUS_BRANCH" ] ; then export OMNIBUS_BRANCH=master ;fi
- echo "token=${BUILD_TRIGGER_TOKEN}" > version_details
- echo "ref=${OMNIBUS_BRANCH}" >> version_details
- echo "variables[ALTERNATIVE_SOURCES]=true" >> version_details
- echo "variables[GITLAB_VERSION]=${CI_COMMIT_SHA}" >> version_details
# Collect version details of all components
- for f in *_VERSION; do echo "variables[$f]=$(cat $f)" >> version_details; done
# Trigger the API and pass values collected above as parameters to it
- cat version_details | tr '\n' '&' | curl -X POST https://gitlab.com/api/v4/projects/20699/trigger/pipeline --data-binary @-
- rm version_details
# Prepare and merge knapsack tests # Prepare and merge knapsack tests
knapsack: knapsack:
<<: *knapsack-state <<: *knapsack-state
...@@ -412,18 +435,6 @@ rake karma: ...@@ -412,18 +435,6 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
bundler:audit:
stage: test
<<: *ruby-static-analysis
<<: *dedicated-runner
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update --ignore CVE-2016-4658"
.migration-paths: &migration-paths .migration-paths: &migration-paths
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
......
This diff is collapsed.
...@@ -256,6 +256,11 @@ gem 'sentry-raven', '~> 2.4.0' ...@@ -256,6 +256,11 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.0'
# I18n
gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
# Metrics # Metrics
group :metrics do group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'allocations', '~> 1.0', require: false, platform: :mri
......
...@@ -198,6 +198,7 @@ GEM ...@@ -198,6 +198,7 @@ GEM
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
fast_gettext (1.4.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.10) ffi (1.9.10)
flay (2.8.1) flay (2.8.1)
...@@ -251,6 +252,16 @@ GEM ...@@ -251,6 +252,16 @@ GEM
gemojione (3.0.1) gemojione (3.0.1)
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gettext (3.2.2)
locale (>= 2.0.5)
text (>= 1.3.0)
gettext_i18n_rails (1.8.0)
fast_gettext (>= 0.9.0)
gettext_i18n_rails_js (1.2.0)
gettext (>= 3.0.2)
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.5.0) gitaly (0.5.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
...@@ -422,6 +433,7 @@ GEM ...@@ -422,6 +433,7 @@ GEM
licensee (8.7.0) licensee (8.7.0)
rugged (~> 0.24) rugged (~> 0.24)
little-plugger (1.1.4) little-plugger (1.1.4)
locale (2.1.2)
logging (2.1.0) logging (2.1.0)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
...@@ -525,6 +537,8 @@ GEM ...@@ -525,6 +537,8 @@ GEM
ast (~> 2.2) ast (~> 2.2)
path_expander (1.0.1) path_expander (1.0.1)
pg (0.18.4) pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
poltergeist (1.9.0) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
...@@ -777,6 +791,7 @@ GEM ...@@ -777,6 +791,7 @@ GEM
temple (0.7.7) temple (0.7.7)
test_after_commit (1.1.0) test_after_commit (1.1.0)
activerecord (>= 3.2) activerecord (>= 3.2)
text (1.3.1)
thin (1.7.0) thin (1.7.0)
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
...@@ -904,6 +919,9 @@ DEPENDENCIES ...@@ -904,6 +919,9 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.5.0) gitaly (~> 0.5.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
......
# GitLab Contributing Process ## GitLab Core Team & GitLab Inc. Contribution Process
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Purpose of describing the contributing process](#purpose-of-describing-the-contributing-process)
- [Common actions](#common-actions)
- [Merge request coaching](#merge-request-coaching)
- [Assigning issues](#assigning-issues)
- [Be kind](#be-kind)
- [Feature freeze on the 7th for the release on the 22nd](#feature-freeze-on-the-7th-for-the-release-on-the-22nd)
- [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective)
- [Kickoff](#kickoff)
- [Copy & paste responses](#copy--paste-responses)
- [Improperly formatted issue](#improperly-formatted-issue)
- [Issue report for old version](#issue-report-for-old-version)
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
- [Code format](#code-format)
- [Issue fixed in newer version](#issue-fixed-in-newer-version)
- [Improperly formatted merge request](#improperly-formatted-merge-request)
- [Inactivity close of an issue](#inactivity-close-of-an-issue)
- [Inactivity close of a merge request](#inactivity-close-of-a-merge-request)
- [Accepting merge requests](#accepting-merge-requests)
- [Only accepting merge requests with green tests](#only-accepting-merge-requests-with-green-tests)
- [Closing down the issue tracker on GitHub](#closing-down-the-issue-tracker-on-github)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
---
## Purpose of describing the contributing process ## Purpose of describing the contributing process
Below we describe the contributing process to GitLab for two reasons. So that Below we describe the contributing process to GitLab for two reasons:
contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.). And so that maintainers know what to expect from contributors 1. Contributors know what to expect from maintainers (possible responses, friendly
(use the latest version, ensure that the issue is addressed, friendly treatment, treatment, etc.)
etc.). 1. Maintainers know what to expect from contributors (use the latest version,
ensure that the issue is addressed, friendly treatment, etc.).
- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) - [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Common actions ## Common actions
### Issue triaging
Our issue triage policies are [described in our handbook]. You are very welcome
to help the GitLab team triage issues. We also organize [issue bash events] once
every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
### Merge request coaching ### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get Several people from the [GitLab team][team] are helping community members to get
...@@ -37,12 +55,6 @@ their contributions accepted by meeting our [Definition of done][done]. ...@@ -37,12 +55,6 @@ their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
## Workflow labels
Labelling issues is described in the [GitLab Inc engineering workflow].
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues ## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover. If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
...@@ -146,6 +158,29 @@ release should have the correct milestone assigned _and_ have the label ...@@ -146,6 +158,29 @@ release should have the correct milestone assigned _and_ have the label
Merge requests without a milestone and this label will Merge requests without a milestone and this label will
not be merged into any stable branches. not be merged into any stable branches.
## Release retrospective and kickoff
### Retrospective
After each release, we have a retrospective call where we discuss what went well,
what went wrong, and what we can improve for the next release. The
[retrospective notes] are public and you are invited to comment on them.
If you're interested, you can even join the
[retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### Kickoff
Before working on the next release, we have a
kickoff call to explain what we expect to ship in the next release. The
[kickoff notes] are public and you are invited to comment on them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call],
on the first working day after the 7th at 6pm CET / 9am PST..
[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
## Copy & paste responses ## Copy & paste responses
### Improperly formatted issue ### Improperly formatted issue
......
...@@ -43,8 +43,8 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { ...@@ -43,8 +43,8 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
const $submitButton = $form.find('input[type=submit], button[type=submit]'); const $submitButton = $form.find('input[type=submit], button[type=submit]');
if (!$submitButton.attr('disabled')) { if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
$submitButton.disable(); $submitButton.disable();
$form.submit();
} }
}); });
......
...@@ -59,7 +59,8 @@ $(() => { ...@@ -59,7 +59,8 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
rootPath: $boardApp.dataset.rootPath, rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail detailIssue: Store.detail,
defaultAvatar: $boardApp.dataset.defaultAvatar,
}, },
computed: { computed: {
detailIssueVisible () { detailIssueVisible () {
...@@ -82,7 +83,7 @@ $(() => { ...@@ -82,7 +83,7 @@ $(() => {
gl.boardService.all() gl.boardService.all()
.then((resp) => { .then((resp) => {
resp.json().forEach((board) => { resp.json().forEach((board) => {
const list = Store.addList(board); const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
......
<<<<<<< HEAD:app/assets/javascripts/boards/models/assignee.js
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
class ListAssignee { class ListAssignee {
...@@ -6,6 +7,14 @@ class ListAssignee { ...@@ -6,6 +7,14 @@ class ListAssignee {
this.name = user.name; this.name = user.name;
this.username = user.username; this.username = user.username;
this.avatarUrl = user.avatar_url; this.avatarUrl = user.avatar_url;
=======
class ListUser {
constructor(user, defaultAvatar) {
this.id = user.id;
this.name = user.name;
this.username = user.username;
this.avatar = user.avatar_url || defaultAvatar;
>>>>>>> 10c1bf2d77fd0ab21309d0b136cbc0ac11f56c77:app/assets/javascripts/boards/models/user.js
} }
} }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import Vue from 'vue'; import Vue from 'vue';
class ListIssue { class ListIssue {
constructor (obj) { constructor (obj, defaultAvatar) {
this.globalId = obj.id; this.globalId = obj.id;
this.id = obj.iid; this.id = obj.iid;
this.title = obj.title; this.title = obj.title;
...@@ -18,6 +18,13 @@ class ListIssue { ...@@ -18,6 +18,13 @@ class ListIssue {
this.selected = false; this.selected = false;
this.position = obj.relative_position || Infinity; this.position = obj.relative_position || Infinity;
<<<<<<< HEAD
=======
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee, defaultAvatar);
}
>>>>>>> 10c1bf2d77fd0ab21309d0b136cbc0ac11f56c77
if (obj.milestone) { if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
} }
......
...@@ -6,7 +6,7 @@ import queryData from '../utils/query_data'; ...@@ -6,7 +6,7 @@ import queryData from '../utils/query_data';
const PER_PAGE = 20; const PER_PAGE = 20;
class List { class List {
constructor (obj) { constructor (obj, defaultAvatar) {
this.id = obj.id; this.id = obj.id;
this._uid = this.guid(); this._uid = this.guid();
this.position = obj.position; this.position = obj.position;
...@@ -18,6 +18,7 @@ class List { ...@@ -18,6 +18,7 @@ class List {
this.loadingMore = false; this.loadingMore = false;
this.issues = []; this.issues = [];
this.issuesSize = 0; this.issuesSize = 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) { if (obj.label) {
this.label = new ListLabel(obj.label); this.label = new ListLabel(obj.label);
...@@ -106,7 +107,7 @@ class List { ...@@ -106,7 +107,7 @@ class List {
createIssues (data) { createIssues (data) {
data.forEach((issueObj) => { data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj)); this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
}); });
} }
......
...@@ -23,8 +23,8 @@ gl.issueBoards.BoardsStore = { ...@@ -23,8 +23,8 @@ gl.issueBoards.BoardsStore = {
this.state.lists = []; this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&'); this.filter.path = gl.utils.getUrlParamsArray().join('&');
}, },
addList (listObj) { addList (listObj, defaultAvatar) {
const list = new List(listObj); const list = new List(listObj, defaultAvatar);
this.state.lists.push(list); this.state.lists.push(list);
return list; return list;
......
...@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', { ...@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', {
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
updateGraphDropdown: false,
}; };
}, },
...@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', { ...@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', {
const pipelines = response.pipelines || response; const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
...@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', { ...@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', {
v-if="shouldRenderTable"> v-if="shouldRenderTable">
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service" /> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div> </div>
</div> </div>
`, `,
......
...@@ -9,9 +9,9 @@ export default { ...@@ -9,9 +9,9 @@ export default {
<span v-if="count === 50" class="events-info pull-right"> <span v-if="count === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip" <i class="fa fa-warning has-tooltip"
aria-hidden="true" aria-hidden="true"
title="Limited to showing 50 events at most" :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)"
data-placement="top"></i> data-placement="top"></i>
Showing 50 events {{ n__('Showing %d event', 'Showing %d events', 50) }}
</span> </span>
`, `,
}; };
...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot; &middot;
<span> <span>
Opened {{ __('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span> </span>
<span> <span>
by {{ __('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span> </span>
</div> </div>
......
...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot; &middot;
<span> <span>
Opened {{ __('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span> </span>
<span> <span>
by {{ __('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> <a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }} {{ issue.author.name }}
</a> </a>
......
...@@ -31,10 +31,10 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -31,10 +31,10 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
</a> </a>
</h5> </h5>
<span> <span>
First {{ __('FirstPushedBy|First') }}
<span class="commit-icon">${iconCommit}</span> <span class="commit-icon">${iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a> <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by {{ __('FirstPushedBy|pushed by') }}
<a :href="commit.author.webUrl" class="commit-author-link"> <a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }} {{ commit.author.name }}
</a> </a>
......
...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot; &middot;
<span> <span>
Opened {{ __('OpenedNDaysAgo|Opened') }}
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span> </span>
<span> <span>
by {{ __('ByAuthor|by') }}
<a :href="issue.author.webUrl" class="issue-author-link"> <a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }} {{ issue.author.name }}
</a> </a>
......
...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -28,11 +28,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot; &middot;
<span> <span>
Opened {{ __('OpenedNDaysAgo|Opened') }}
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span> </span>
<span> <span>
by {{ __('ByAuthor|by') }}
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span> </span>
<template v-if="mergeRequest.state === 'closed'"> <template v-if="mergeRequest.state === 'closed'">
......
...@@ -32,7 +32,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -32,7 +32,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
</h5> </h5>
<span> <span>
<a :href="build.url" class="build-date">{{ build.date }}</a> <a :href="build.url" class="build-date">{{ build.date }}</a>
by {{ __('ByAuthor|by') }}
<a :href="build.author.webUrl" class="issue-author-link"> <a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }} {{ build.author.name }}
</a> </a>
......
...@@ -12,10 +12,10 @@ global.cycleAnalytics.TotalTimeComponent = Vue.extend({ ...@@ -12,10 +12,10 @@ global.cycleAnalytics.TotalTimeComponent = Vue.extend({
template: ` template: `
<span class="total-time"> <span class="total-time">
<template v-if="Object.keys(time).length"> <template v-if="Object.keys(time).length">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template> <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template> <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template> <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template> <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template>
</template> </template>
<template v-else> <template v-else>
-- --
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Translate from '../vue_shared/translate';
import LimitWarningComponent from './components/limit_warning_component'; import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component'); require('./components/stage_code_component');
...@@ -16,6 +17,8 @@ require('./cycle_analytics_service'); ...@@ -16,6 +17,8 @@ require('./cycle_analytics_service');
require('./cycle_analytics_store'); require('./cycle_analytics_store');
require('./default_event_objects'); require('./default_event_objects');
Vue.use(Translate);
$(() => { $(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
......
...@@ -30,7 +30,7 @@ class CycleAnalyticsService { ...@@ -30,7 +30,7 @@ class CycleAnalyticsService {
startDate, startDate,
} = options; } = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, { return $.get(`${this.requestPath}/events/${stage.name}.json`, {
cycle_analytics: { cycle_analytics: {
start_date: startDate, start_date: startDate,
}, },
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { __ } from '../locale';
require('../lib/utils/text_utility'); require('../lib/utils/text_utility');
const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
...@@ -7,13 +8,13 @@ const global = window.gl || (window.gl = {}); ...@@ -7,13 +8,13 @@ const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = { const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'),
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'),
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', code: __('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'),
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', test: __('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'),
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', review: __('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'),
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', staging: __('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'),
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'),
}; };
global.cycleAnalytics.CycleAnalyticsStore = { global.cycleAnalytics.CycleAnalyticsStore = {
...@@ -38,7 +39,7 @@ global.cycleAnalytics.CycleAnalyticsStore = { ...@@ -38,7 +39,7 @@ global.cycleAnalytics.CycleAnalyticsStore = {
}); });
newData.stages.forEach((item) => { newData.stages.forEach((item) => {
const stageSlug = gl.text.dasherize(item.title.toLowerCase()); const stageSlug = gl.text.dasherize(item.name.toLowerCase());
item.active = false; item.active = false;
item.isUserAllowed = data.permissions[stageSlug]; item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
......
<script>
import eventHub from '../eventhub';
export default {
data() {
return {
isLoading: false,
};
},
props: {
deployKey: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
},
methods: {
doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey);
},
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
};
</script>
<template>
<button
class="btn btn-sm prepend-left-10"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
{{ text }}
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="Loading">
</i>
</button>
</template>
<script>
/* global Flash */
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
export default {
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
props: {
endpoint: {
type: String,
required: true,
},
},
computed: {
hasKeys() {
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
},
components: {
keysPanel,
},
methods: {
fetchKeys() {
this.isLoading = true;
this.service.getKeys()
.then((data) => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => new Flash('Error getting deploy keys'));
},
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error removing deploy key'));
}
},
},
created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
};
</script>
<template>
<div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys">
<div
class="text-center"
v-if="isLoading && !hasKeys">
<i
class="fa fa-spinner fa-spin fa-2x"
aria-hidden="true"
aria-label="Loading deploy keys">
</i>
</div>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
:keys="keys.enabled_keys"
:store="store" />
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store" />
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" />
</div>
</div>
</template>
<script>
import actionBtn from './action_btn.vue';
export default {
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
},
components: {
actionBtn,
},
computed: {
timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at);
},
},
methods: {
isEnabled(id) {
return this.store.findEnabledKey(id) !== undefined;
},
},
};
</script>
<template>
<div>
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon">
</i>
</div>
<div class="deploy-key-content key-list-item-info">
<strong class="title">
{{ deployKey.title }}
</strong>
<div class="description">
{{ deployKey.fingerprint }}
</div>
<div
v-if="deployKey.can_push"
class="write-access-allowed">
Write access allowed
</div>
</div>
<div class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="project in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path">
{{ project.full_name }}
</a>
</div>
<div class="deploy-key-content">
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" />
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable" />
</div>
</div>
</template>
<script>
import key from './key.vue';
export default {
props: {
title: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
},
components: {
key,
},
};
</script>
<template>
<div class="deploy-keys-panel">
<h5>
{{ title }}
({{ keys.length }})
</h5>
<ul class="well-list"
v-if="keys.length">
<li
v-for="deployKey in keys"
:key="deployKey.id">
<key
:deploy-key="deployKey"
:store="store" />
</li>
</ul>
<div
class="settings-message text-center"
v-else-if="showHelpBox">
No deploy keys found. Create one with the form above.
</div>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: document.getElementById('js-deploy-keys'),
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
components: {
deployKeysApp,
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
},
});
},
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
}
getKeys() {
return this.resource.get()
.then(response => response.json());
}
enableKey(id) {
return this.resource.enable({ id }, {});
}
disableKey(id) {
return this.resource.disable({ id }, {});
}
}
export default class DeployKeysStore {
constructor() {
this.keys = {};
}
findEnabledKey(id) {
return this.keys.enabled_keys.find(key => key.id === id);
}
}
...@@ -50,6 +50,7 @@ import UserCallout from './user_callout'; ...@@ -50,6 +50,7 @@ import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki'; import ShortcutsWiki from './shortcuts_wiki';
import BlobViewer from './blob/viewer/index'; import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -198,6 +199,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -198,6 +199,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
new gl.IssuableTemplateSelectors(); new gl.IssuableTemplateSelectors();
new AutoWidthDropdownSelect($('.js-target-branch-select')).init();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
...@@ -344,6 +346,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -344,6 +346,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
new BuildArtifacts(); new BuildArtifacts();
break; break;
case 'projects:artifacts:file':
new BlobViewer();
break;
case 'help:index': case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break; break;
......
...@@ -5,7 +5,7 @@ require('./preview_markdown'); ...@@ -5,7 +5,7 @@ require('./preview_markdown');
window.DropzoneInput = (function() { window.DropzoneInput = (function() {
function DropzoneInput(form) { function DropzoneInput(form) {
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() { ...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() {
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"; btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
project_uploads_path = window.project_uploads_path || null; uploads_path = window.uploads_path || null;
max_file_size = gon.max_file_size || 10; max_file_size = gon.max_file_size || 10;
form_textarea = $(form).find(".js-gfm-input"); form_textarea = $(form).find(".js-gfm-input");
form_textarea.wrap("<div class=\"div-dropzone\"></div>"); form_textarea.wrap("<div class=\"div-dropzone\"></div>");
...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() { ...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() {
"display": "none" "display": "none"
}); });
if (!project_uploads_path) return; if (!uploads_path) return;
dropzone = form_dropzone.dropzone({ dropzone = form_dropzone.dropzone({
url: project_uploads_path, url: uploads_path,
dictDefaultMessage: "", dictDefaultMessage: "",
clickable: true, clickable: true,
paramName: "file", paramName: "file",
...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() { ...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() {
formData = new FormData(); formData = new FormData();
formData.append("file", item, filename); formData.append("file", item, filename);
return $.ajax({ return $.ajax({
url: project_uploads_path, url: uploads_path,
type: "POST", type: "POST",
data: formData, data: formData,
dataType: "json", dataType: "json",
......
<script> <script>
/* eslint-disable no-new */
/* global Flash */ /* global Flash */
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue'; import EnvironmentTable from './environments_table.vue';
...@@ -71,11 +69,13 @@ export default { ...@@ -71,11 +69,13 @@ export default {
eventHub.$on('refreshEnvironments', this.fetchEnvironments); eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder); eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
}, },
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshEnvironments'); eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder'); eventHub.$off('toggleFolder');
eventHub.$off('postAction');
}, },
methods: { methods: {
...@@ -122,6 +122,7 @@ export default { ...@@ -122,6 +122,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
...@@ -137,9 +138,16 @@ export default { ...@@ -137,9 +138,16 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoadingFolderContent = false; this.isLoadingFolderContent = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
},
}, },
}; };
</script> </script>
...@@ -217,7 +225,6 @@ export default { ...@@ -217,7 +225,6 @@ export default {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" /> :is-loading-folder-content="isLoadingFolderContent" />
</div> </div>
......
<script> <script>
/* global Flash */
/* eslint-disable no-new */
import playIconSvg from 'icons/_icon_play.svg'; import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -12,11 +9,6 @@ export default { ...@@ -12,11 +9,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
service: {
type: Object,
required: true,
},
}, },
data() { data() {
...@@ -38,15 +30,7 @@ export default { ...@@ -38,15 +30,7 @@ export default {
$(this.$refs.tooltip).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) eventHub.$emit('postAction', endpoint);
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
isActionDisabled(action) { isActionDisabled(action) {
......
...@@ -46,11 +46,6 @@ export default { ...@@ -46,11 +46,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
service: {
type: Object,
required: true,
},
}, },
computed: { computed: {
...@@ -543,31 +538,34 @@ export default { ...@@ -543,31 +538,34 @@ export default {
<actions-component <actions-component
v-if="hasManualActions && canCreateDeployment" v-if="hasManualActions && canCreateDeployment"
:service="service" :actions="manualActions"
:actions="manualActions"/> />
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> :external-url="externalURL"
/>
<monitoring-button-component <monitoring-button-component
v-if="monitoringUrl && canReadEnvironment" v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/> :monitoring-url="monitoringUrl"
/>
<terminal-button-component <terminal-button-component
v-if="model && model.terminal_path" v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/> :terminal-path="model.terminal_path"
/>
<stop-component <stop-component
v-if="hasStopAction && canCreateDeployment" v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path" :stop-url="model.stop_path"
:service="service"/> />
<rollback-component <rollback-component
v-if="canRetry && canCreateDeployment" v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :retry-url="retryUrl"
:service="service"/> />
</div> </div>
</td> </td>
</tr> </tr>
......
<script> <script>
/* global Flash */
/* eslint-disable no-new */
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
...@@ -20,11 +18,6 @@ export default { ...@@ -20,11 +18,6 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
service: {
type: Object,
required: true,
},
}, },
data() { data() {
...@@ -37,17 +30,7 @@ export default { ...@@ -37,17 +30,7 @@ export default {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); eventHub.$emit('postAction', this.retryUrl);
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
}, },
}; };
......
<script> <script>
/* global Flash */
/* eslint-disable no-new, no-alert */
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
...@@ -13,11 +11,6 @@ export default { ...@@ -13,11 +11,6 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
service: {
type: Object,
required: true,
},
}, },
data() { data() {
...@@ -34,20 +27,13 @@ export default { ...@@ -34,20 +27,13 @@ export default {
methods: { methods: {
onClick() { onClick() {
// eslint-disable-next-line no-alert
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); $(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) eventHub.$emit('postAction', this.stopUrl);
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.', 'alert');
});
} }
}, },
}, },
......
...@@ -28,11 +28,6 @@ export default { ...@@ -28,11 +28,6 @@ export default {
default: false, default: false,
}, },
service: {
type: Object,
required: true,
},
isLoadingFolderContent: { isLoadingFolderContent: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -78,7 +73,7 @@ export default { ...@@ -78,7 +73,7 @@ export default {
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:service="service" /> />
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent"> <tr v-if="isLoadingFolderContent">
...@@ -96,7 +91,7 @@ export default { ...@@ -96,7 +91,7 @@ export default {
:model="children" :model="children"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:service="service" /> />
<tr> <tr>
<td <td
......
<script> <script>
/* eslint-disable no-new */
/* global Flash */ /* global Flash */
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentTable from '../components/environments_table.vue';
...@@ -99,6 +98,7 @@ export default { ...@@ -99,6 +98,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.', 'alert'); new Flash('An error occurred while fetching the environments.', 'alert');
}); });
}, },
...@@ -169,7 +169,7 @@ export default { ...@@ -169,7 +169,7 @@ export default {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:service="service"/> />
<table-pagination <table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
...@@ -101,9 +101,17 @@ window.gl.GfmAutoComplete = { ...@@ -101,9 +101,17 @@ window.gl.GfmAutoComplete = {
} }
} }
}, },
setup: function(input) { setup: function(input, enableMap = {
emojis: true,
members: true,
issues: true,
milestones: true,
mergeRequests: true,
labels: true
}) {
// Add GFM auto-completion to all input fields, that accept GFM input. // Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input'); this.input = input || $('.js-gfm-input');
this.enableMap = enableMap;
this.setupLifecycle(); this.setupLifecycle();
}, },
setupLifecycle() { setupLifecycle() {
...@@ -115,7 +123,84 @@ window.gl.GfmAutoComplete = { ...@@ -115,7 +123,84 @@ window.gl.GfmAutoComplete = {
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
}); });
}, },
setupAtWho: function($input) { setupAtWho: function($input) {
if (this.enableMap.emojis) this.setupEmoji($input);
if (this.enableMap.members) this.setupMembers($input);
if (this.enableMap.issues) this.setupIssues($input);
if (this.enableMap.milestones) this.setupMilestones($input);
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($input);
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
displayTpl: function(value) {
if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
}
tpl += '</li>';
return _.template(tpl)(value);
}.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
if (value.params.length > 0) {
reference_prefix = value.params[0][0];
if (/^[@%~]/.test(reference_prefix)) {
tpl += '<%- reference_prefix %>';
}
}
return _.template(tpl)({ reference_prefix: reference_prefix });
},
suffix: '',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
search = search + " " + c.aliases.join(" ");
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
search: search
};
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
} else {
return null;
}
}
}
});
return;
},
setupEmoji($input) {
// Emoji // Emoji
$input.atwho({ $input.atwho({
at: ':', at: ':',
...@@ -139,6 +224,9 @@ window.gl.GfmAutoComplete = { ...@@ -139,6 +224,9 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
},
setupMembers($input) {
// Team Members // Team Members
$input.atwho({ $input.atwho({
at: '@', at: '@',
...@@ -180,6 +268,9 @@ window.gl.GfmAutoComplete = { ...@@ -180,6 +268,9 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
},
setupIssues($input) {
$input.atwho({ $input.atwho({
at: '#', at: '#',
alias: 'issues', alias: 'issues',
...@@ -208,6 +299,9 @@ window.gl.GfmAutoComplete = { ...@@ -208,6 +299,9 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
},
setupMilestones($input) {
$input.atwho({ $input.atwho({
at: '%', at: '%',
alias: 'milestones', alias: 'milestones',
...@@ -236,6 +330,9 @@ window.gl.GfmAutoComplete = { ...@@ -236,6 +330,9 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
},
setupMergeRequests($input) {
$input.atwho({ $input.atwho({
at: '!', at: '!',
alias: 'mergerequests', alias: 'mergerequests',
...@@ -264,6 +361,9 @@ window.gl.GfmAutoComplete = { ...@@ -264,6 +361,9 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
},
setupLabels($input) {
$input.atwho({ $input.atwho({
at: '~', at: '~',
alias: 'labels', alias: 'labels',
...@@ -298,73 +398,8 @@ window.gl.GfmAutoComplete = { ...@@ -298,73 +398,8 @@ window.gl.GfmAutoComplete = {
} }
} }
}); });
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
displayTpl: function(value) {
if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
}
if (value.params.length > 0) {
tpl += ' <small><%- params.join(" ") %></small>';
}
if (value.description !== '') {
tpl += '<small class="description"><i><%- description %></i></small>';
}
tpl += '</li>';
return _.template(tpl)(value);
}.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
if (value.params.length > 0) {
reference_prefix = value.params[0][0];
if (/^[@%~]/.test(reference_prefix)) {
tpl += '<%- reference_prefix %>';
}
}
return _.template(tpl)({ reference_prefix: reference_prefix });
},
suffix: '',
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(commands) {
if (gl.GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, function(c) {
var search = c.name;
if (c.aliases.length > 0) {
search = search + " " + c.aliases.join(" ");
}
return {
name: c.name,
aliases: c.aliases,
params: c.params,
description: c.description,
search: search
};
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
} else {
return null;
}
}
}
});
return;
}, },
fetchData: function($input, at) { fetchData: function($input, at) {
if (this.isLoadingData[at]) return; if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
......
let instanceCount = 0;
class AutoWidthDropdownSelect {
constructor(selectElement) {
this.$selectElement = $(selectElement);
this.dropdownClass = `js-auto-width-select-dropdown-${instanceCount}`;
instanceCount += 1;
}
init() {
const dropdownClass = this.dropdownClass;
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
dropdownCss() {
let resultantWidth = 'auto';
const $dropdown = $(`.${dropdownClass}`);
// We have to look at the parent because
// `offsetParent` on a `display: none;` is `null`
const offsetParentWidth = $(this).parent().offsetParent().width();
// Reset any width to let it naturally flow
$dropdown.css('width', 'auto');
if ($dropdown.outerWidth(false) > offsetParentWidth) {
resultantWidth = offsetParentWidth;
}
return {
width: resultantWidth,
maxWidth: offsetParentWidth,
};
},
});
return this;
}
}
export default AutoWidthDropdownSelect;
...@@ -35,6 +35,14 @@ ...@@ -35,6 +35,14 @@
}); });
}; };
w.gl.utils.ajaxPost = function(url, data) {
return $.ajax({
type: 'POST',
url: url,
data: data,
});
};
w.gl.utils.extractLast = function(term) { w.gl.utils.extractLast = function(term) {
return this.split(term).pop(); return this.split(term).pop();
}; };
......
var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:37-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file
var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file
var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d día","Últimos %d días"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["día","días"]}}};
\ No newline at end of file
import Jed from 'jed';
/**
This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages
**/
function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
const locales = allLocales.reduce((d, obj) => {
const data = d;
const localeKey = Object.keys(obj)[0];
data[localeKey] = obj[localeKey];
return data;
}, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en';
lang = lang.replace(/-/g, '_');
const locale = new Jed(locales[lang]);
/**
Translates `text`
@param text The text to be translated
@returns {String} The translated text
**/
const gettext = locale.gettext.bind(locale);
/**
Translate the text with a number
if the number is more than 1 it will use the `pluralText` translation.
This method allows for contexts, see below re. contexts
@param text Singular text to translate (eg. '%d day')
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
**/
const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
return translated[translated.length - 1];
};
/**
Translate context based text
Either pass in the context translation like `Context|Text to translate`
or allow for dynamic text by doing passing in the context first & then the text to translate
@param keyOrContext Can be either the key to translate including the context
(eg. 'Context|Text') or just the context for the translation
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
**/
const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|');
return translated[translated.length - 1];
};
export { lang };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export default locale;
...@@ -291,7 +291,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -291,7 +291,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
MergeRequestWidget.prototype.updateCommitUrls = function(id) { MergeRequestWidget.prototype.updateCommitUrls = function(id) {
const commitsUrl = this.opts.commits_path; const commitsUrl = this.opts.commits_path;
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); $('.js-commit-link').text(id).attr('href', [commitsUrl, id].join('/'));
}; };
MergeRequestWidget.prototype.initMiniPipelineGraph = function() { MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
filterByText: true, filterByText: true,
remote: false, remote: false,
fieldName: $branchSelect.data('field-name'), fieldName: $branchSelect.data('field-name'),
filterInput: 'input[type="search"]',
selectable: true, selectable: true,
isSelectable: function(branch, $el) { isSelectable: function(branch, $el) {
return !$el.hasClass('is-active'); return !$el.hasClass('is-active');
...@@ -50,6 +51,21 @@ ...@@ -50,6 +51,21 @@
} }
} }
}); });
const $dropdownContainer = $branchSelect.closest('.dropdown');
const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$filterInput.on('keyup', (e) => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
$fieldInput.val(text);
$('.dropdown-toggle-text', $branchSelect).text(text);
$dropdownContainer.removeClass('open');
});
}; };
NewBranchForm.prototype.setupRestrictions = function() { NewBranchForm.prototype.setupRestrictions = function() {
......
This diff is collapsed.
<script>
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
/* global Flash */ /* global Flash */
import StatusIconEntityMap from '../../ci_status_icons'; import StatusIconEntityMap from '../../ci_status_icons';
...@@ -7,36 +22,55 @@ export default { ...@@ -7,36 +22,55 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
builds: '', isLoading: false,
spinner: '<span class="fa fa-spinner fa-spin"></span>', dropdownContent: '',
endpoint: this.stage.dropdown_path,
}; };
}, },
updated() { updated() {
if (this.builds) { if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
} }
}, },
methods: { watch: {
fetchBuilds(e) { updateDropdown() {
const ariaExpanded = e.currentTarget.attributes['aria-expanded']; if (this.updateDropdown &&
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs();
}
},
},
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null; methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
this.isLoading = true;
this.fetchJobs();
}
},
return this.$http.get(this.stage.dropdown_path) fetchJobs() {
this.$http.get(this.endpoint)
.then((response) => { .then((response) => {
this.builds = JSON.parse(response.body).html; this.dropdownContent = response.json().html;
this.isLoading = false;
}) })
.catch(() => { .catch(() => {
// If dropdown is opened we'll close it. this.closeDropdown();
if (this.$el.classList.contains('open')) { this.isLoading = false;
$(this.$refs.dropdown).dropdown('toggle');
}
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
return flash; return flash;
...@@ -57,59 +91,83 @@ export default { ...@@ -57,59 +91,83 @@ export default {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
}, },
computed: { computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() { dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container'; return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
}, },
triggerButtonClass() { triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; return `ci-status-icon-${this.stage.status.group}`;
}, },
svgHTML() {
svgIcon() {
return StatusIconEntityMap[this.stage.status.icon]; return StatusIconEntityMap[this.stage.status.icon];
}, },
}, },
template: ` };
<div> </script>
<button
@click="fetchBuilds($event)" <template>
:class="triggerButtonClass" <div class="dropdown">
:title="stage.title" <button
data-placement="top" :class="triggerButtonClass"
data-toggle="dropdown" @click="onClickStage"
type="button" class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
:aria-label="stage.title" :title="stage.title"
ref="dropdown"> data-placement="top"
<span data-toggle="dropdown"
v-html="svgHTML" type="button"
aria-hidden="true"> id="stageDropdown"
</span> aria-haspopup="true"
<i aria-expanded="false">
class="fa fa-caret-down"
aria-hidden="true" /> <span
</button> v-html="svgIcon"
<ul aria-hidden="true"
ref="dropdown-content" :aria-label="stage.title">
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> </span>
<div
class="arrow-up" <i
aria-hidden="true"></div> class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown">
<li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu">
<div <div
:class="dropdownClass" class="text-center"
class="js-builds-dropdown-list scrollable-menu" v-if="isLoading">
v-html="buildsOrSpinner"> <i
class="fa fa-spin fa-spinner"
aria-hidden="true"
aria-label="Loading">
</i>
</div> </div>
</ul>
</div> <ul
`, v-else
}; v-html="dropdownContent">
</ul>
</li>
</ul>
</div>
</script>
...@@ -49,6 +49,7 @@ export default { ...@@ -49,6 +49,7 @@ export default {
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
updateGraphDropdown: false,
}; };
}, },
...@@ -198,15 +199,21 @@ export default { ...@@ -198,15 +199,21 @@ export default {
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
...@@ -263,7 +270,9 @@ export default { ...@@ -263,7 +270,9 @@ export default {
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service"/> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div> </div>
<gl-pagination <gl-pagination
......
...@@ -40,6 +40,6 @@ export default class PipelinesService { ...@@ -40,6 +40,6 @@ export default class PipelinesService {
* @return {Promise} * @return {Promise}
*/ */
postAction(endpoint) { postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true }); return Vue.http.post(`${endpoint}.json`);
} }
} }
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview
// and showing a warning when more than `x` users are referenced. // (including the explanation of slash commands), and showing a warning when
// more than `x` users are referenced.
// //
(function () { (function () {
var lastTextareaPreviewed; var lastTextareaPreviewed;
...@@ -17,32 +18,45 @@ ...@@ -17,32 +18,45 @@
// Minimum number of users referenced before triggering a warning // Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10; MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText; var mdText;
var preview = $form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) { if (preview.hasClass('md-preview-loading')) {
return; return;
} }
mdText = $form.find('textarea.markdown-area').val(); mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text('Nothing to preview.'); preview.text(this.emptyMessage);
this.hideReferencedUsers($form); this.hideReferencedUsers($form);
} else { } else {
preview.addClass('md-preview-loading').text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
this.fetchMarkdownPreview(mdText, (function (response) { this.fetchMarkdownPreview(mdText, url, (function (response) {
preview.removeClass('md-preview-loading').html(response.body); var body;
if (response.body.length > 0) {
body = response.body;
} else {
body = this.emptyMessage;
}
preview.removeClass('md-preview-loading').html(body);
preview.renderGFM(); preview.renderGFM();
this.renderReferencedUsers(response.references.users, $form); this.renderReferencedUsers(response.references.users, $form);
if (response.references.commands) {
this.renderReferencedCommands(response.references.commands, $form);
}
}).bind(this)); }).bind(this));
} }
}; };
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!window.preview_markdown_path) { if (!url) {
return; return;
} }
if (text === this.ajaxCache.text) { if (text === this.ajaxCache.text) {
...@@ -51,7 +65,7 @@ ...@@ -51,7 +65,7 @@
} }
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: window.preview_markdown_path, url: url,
data: { data: {
text: text text: text
}, },
...@@ -83,6 +97,22 @@ ...@@ -83,6 +97,22 @@
} }
}; };
MarkdownPreview.prototype.hideReferencedCommands = function ($form) {
$form.find('.referenced-commands').hide();
};
MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) {
var referencedCommands;
referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) {
referencedCommands.html(commands);
referencedCommands.show();
} else {
referencedCommands.html('');
referencedCommands.hide();
}
};
return MarkdownPreview; return MarkdownPreview;
}()); }());
...@@ -137,6 +167,8 @@ ...@@ -137,6 +167,8 @@
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
markdownPreview.hideReferencedCommands($form);
}); });
$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
......
...@@ -10,13 +10,18 @@ export default { ...@@ -10,13 +10,18 @@ export default {
pipelines: { pipelines: {
type: Array, type: Array,
required: true, required: true,
default: () => ([]),
}, },
service: { service: {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
components: { components: {
...@@ -40,7 +45,9 @@ export default { ...@@ -40,7 +45,9 @@ export default {
v-bind:model="model"> v-bind:model="model">
<tr is="pipelines-table-row-component" <tr is="pipelines-table-row-component"
:pipeline="model" :pipeline="model"
:service="service"></tr> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template> </template>
</tbody> </tbody>
</table> </table>
......
...@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; ...@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status'; import PipelinesStatusComponent from '../../pipelines/components/status';
import PipelinesStageComponent from '../../pipelines/components/stage'; import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit'; import CommitComponent from './commit';
...@@ -24,6 +24,12 @@ export default { ...@@ -24,6 +24,12 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
components: { components: {
...@@ -213,7 +219,10 @@ export default { ...@@ -213,7 +219,10 @@ export default {
<div class="stage-container dropdown js-mini-pipeline-graph" <div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0" v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages"> v-for="stage in pipeline.details.stages">
<dropdown-stage :stage="stage"/>
<dropdown-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"/>
</div> </div>
</td> </td>
......
import {
__,
n__,
s__,
} from '../locale';
export default (Vue) => {
Vue.mixin({
methods: {
/**
Translates `text`
@param text The text to be translated
@returns {String} The translated text
**/
__,
/**
Translate the text with a number
if the number is more than 1 it will use the `pluralText` translation.
This method allows for contexts, see below re. contexts
@param text Singular text to translate (eg. '%d day')
@param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days')
**/
n__,
/**
Translate context based text
Either pass in the context translation like `Context|Text to translate`
or allow for dynamic text by doing passing in the context first & then the text to translate
@param keyOrContext Can be either the key to translate including the context
(eg. 'Context|Text') or just the context for the translation
(eg. 'Context')
@param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text
**/
s__,
},
});
};
...@@ -159,3 +159,31 @@ a { ...@@ -159,3 +159,31 @@ a {
.fade-in { .fade-in {
animation: fadeIn $fade-in-duration 1; animation: fadeIn $fade-in-duration 1;
} }
@keyframes fadeInHalf {
0% {
opacity: 0;
}
100% {
opacity: 0.5;
}
}
.fade-in-half {
animation: fadeInHalf $fade-in-duration 1;
}
@keyframes fadeInFull {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.fade-in-full {
animation: fadeInFull $fade-in-duration 1;
}
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
} }
.stage-nav-item { .stage-nav-item {
display: block; display: flex;
line-height: 65px; line-height: 65px;
border-top: 1px solid transparent; border-top: 1px solid transparent;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
...@@ -209,14 +209,10 @@ ...@@ -209,14 +209,10 @@
} }
.stage-nav-item-cell { .stage-nav-item-cell {
float: left;
&.stage-name {
width: 65%;
}
&.stage-median { &.stage-median {
width: 35%; margin-left: auto;
margin-right: $gl-padding;
min-width: calc(35% - #{$gl-padding});
} }
} }
......
...@@ -482,6 +482,10 @@ ...@@ -482,6 +482,10 @@
} }
} }
.target-branch-select-dropdown-container {
position: relative;
}
.assign-to-me-link { .assign-to-me-link {
padding-left: 12px; padding-left: 12px;
white-space: nowrap; white-space: nowrap;
......
...@@ -57,6 +57,25 @@ ul.notes { ...@@ -57,6 +57,25 @@ ul.notes {
position: relative; position: relative;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
.note-headline-light,
.fa-spinner {
margin-left: 3px;
}
}
&.note-discussion { &.note-discussion {
&.timeline-entry { &.timeline-entry {
padding: 14px 10px; padding: 14px 10px;
...@@ -687,6 +706,10 @@ ul.notes { ...@@ -687,6 +706,10 @@ ul.notes {
} }
} }
.discussion-notes .flash-container {
margin-bottom: 0;
}
// Merge request notes in diffs // Merge request notes in diffs
.diff-file { .diff-file {
// Diff is side by side // Diff is side by side
......
...@@ -781,16 +781,11 @@ ...@@ -781,16 +781,11 @@
} }
.scrollable-menu { .scrollable-menu {
padding: 0;
max-height: 245px; max-height: 245px;
overflow: auto; overflow: auto;
} }
// Loading icon
.builds-dropdown-loading {
margin: 0 auto;
width: 20px;
}
// Action icon on the right // Action icon on the right
a.ci-action-icon-wrapper { a.ci-action-icon-wrapper {
color: $action-icon-color; color: $action-icon-color;
...@@ -893,30 +888,29 @@ ...@@ -893,30 +888,29 @@
* Top arrow in the dropdown in the mini pipeline graph * Top arrow in the dropdown in the mini pipeline graph
*/ */
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
.arrow-up {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::before { &::before,
border-width: 0 5px 5px; &::after {
border-bottom-color: $border-color; content: '';
} display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::after { &::before {
margin-top: 1px; border-width: 0 5px 5px;
border-bottom-color: $white-light; border-bottom-color: $border-color;
} }
&::after {
margin-top: 1px;
border-bottom-color: $white-light;
} }
} }
......
...@@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base ...@@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
around_action :set_locale
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :can?, :current_application_settings helper_method :can?, :current_application_settings
...@@ -269,4 +271,12 @@ class ApplicationController < ActionController::Base ...@@ -269,4 +271,12 @@ class ApplicationController < ActionController::Base
def u2f_app_id def u2f_app_id
request.base_url request.base_url
end end
def set_locale
Gitlab::I18n.set_locale(current_user)
yield
ensure
Gitlab::I18n.reset_locale
end
end end
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
module UploadsActions
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 unless uploader.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
end
...@@ -67,7 +67,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -67,7 +67,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def omniauth_error def omniauth_error
@provider = params[:provider] @provider = params[:provider]
@error = params[:error] @error = params[:error]
render 'errors/omniauth_error', layout: "errors", status: 422 render 'errors/omniauth_error', layout: "oauth_error", status: 422
end end
def cas3 def cas3
......
...@@ -85,7 +85,8 @@ class ProfilesController < Profiles::ApplicationController ...@@ -85,7 +85,8 @@ class ProfilesController < Profiles::ApplicationController
:twitter, :twitter,
:username, :username,
:website_url, :website_url,
:organization :organization,
:preferred_language
) )
end end
end end
...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController ...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user) return render_404 unless @project.feature_available?(:builds, current_user)
end end
def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled
end
end end
class Projects::ArtifactsController < Projects::ApplicationController class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include RendersBlob
layout 'project' layout 'project'
before_action :authorize_read_build! before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep] before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path before_action :extract_ref_name_and_path
before_action :validate_artifacts! before_action :validate_artifacts!
before_action :set_path_and_entry, only: [:file, :raw]
def download def download
if artifacts_file.file_storage? if artifacts_file.file_storage?
...@@ -24,15 +26,24 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -24,15 +26,24 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def file def file
entry = build.artifacts_metadata_entry(params[:path]) blob = @entry.blob
override_max_blob_size(blob)
if entry.exists? respond_to do |format|
send_artifacts_entry(build, entry) format.html do
else render 'file'
render_404 end
format.json do
render_blob_json(blob)
end
end end
end end
def raw
send_artifacts_entry(build, @entry)
end
def keep def keep
build.keep_artifacts! build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build) redirect_to namespace_project_build_path(project.namespace, project, build)
...@@ -81,4 +92,11 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -81,4 +92,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
def artifacts_file def artifacts_file
@artifacts_file ||= build.artifacts_file @artifacts_file ||= build.artifacts_file
end end
def set_path_and_entry
@path = params[:path]
@entry = build.artifacts_metadata_entry(@path)
render_404 unless @entry.exists?
end
end end
...@@ -8,7 +8,12 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -8,7 +8,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings" layout "project_settings"
def index def index
redirect_to_repository_settings(@project) respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json do
render json: Projects::Settings::DeployKeysPresenter.new(@project, current_user: current_user).as_json
end
end
end end
def new def new
...@@ -19,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -19,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
@key = DeployKey.new(deploy_key_params.merge(user: current_user)) @key = DeployKey.new(deploy_key_params.merge(user: current_user))
unless @key.valid? && @project.deploy_keys << @key unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe flash[:alert] = @key.errors.full_messages.join(', ').html_safe
end end
redirect_to_repository_settings(@project) redirect_to_repository_settings(@project)
end end
...@@ -27,7 +32,10 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -27,7 +32,10 @@ class Projects::DeployKeysController < Projects::ApplicationController
def enable def enable
Projects::EnableDeployKeyService.new(@project, current_user, params).execute Projects::EnableDeployKeyService.new(@project, current_user, params).execute
redirect_to_repository_settings(@project) respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json { head :ok }
end
end end
def disable def disable
...@@ -35,7 +43,11 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -35,7 +43,11 @@ class Projects::DeployKeysController < Projects::ApplicationController
return render_404 unless deploy_key_project return render_404 unless deploy_key_project
deploy_key_project.destroy! deploy_key_project.destroy!
redirect_to_repository_settings(@project)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.json { head :ok }
end
end end
protected protected
......
class Projects::PagesController < Projects::ApplicationController class Projects::PagesController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_read_pages!, only: [:show] before_action :authorize_read_pages!, only: [:show]
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
......
class Projects::PagesDomainsController < Projects::ApplicationController class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy] before_action :domain, only: [:show, :destroy]
......
...@@ -6,6 +6,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -6,6 +6,8 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :builds_enabled, only: :charts before_action :builds_enabled, only: :charts
wrap_parameters Ci::Pipeline
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
...@@ -92,13 +94,25 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -92,13 +94,25 @@ class Projects::PipelinesController < Projects::ApplicationController
def retry def retry
pipeline.retry_failed(current_user) pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) respond_to do |format|
format.html do
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
format.json { head :no_content }
end
end end
def cancel def cancel
pipeline.cancel_running pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) respond_to do |format|
format.html do
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
format.json { head :no_content }
end
end end
def charts def charts
......
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create]
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
end
private private
def uploader def uploader
...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController
def image_or_video? def image_or_video?
uploader && uploader.file.exists? && uploader.image_or_video? uploader && uploader.file.exists? && uploader.image_or_video?
end end
def uploader_class
FileUploader
end
alias_method :model, :project
end end
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -97,9 +95,14 @@ class Projects::WikisController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } result = PreviewMarkdownService.new(@project, current_user, params).execute
render_markdown_preview(params[:text], context) render json: {
body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: result[:users]
}
}
end end
private private
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController ...@@ -240,7 +239,15 @@ class ProjectsController < Projects::ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text]) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text]),
references: {
users: result[:users],
commands: view_context.markdown(result[:commands])
}
}
end end
private private
......
...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController ...@@ -3,7 +3,6 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
include RendersBlob include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController ...@@ -90,7 +89,14 @@ class SnippetsController < ApplicationController
end end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true) result = PreviewMarkdownService.new(@project, current_user, params).execute
render json: {
body: view_context.markdown(result[:text], skip_project_check: true),
references: {
users: result[:users]
}
}
end end
protected protected
......
class UploadsController < ApplicationController class UploadsController < ApplicationController
skip_before_action :authenticate_user! include UploadsActions
before_action :find_model, :authorize_access!
def show
uploader = @model.send(upload_mount)
unless uploader.file_storage?
return redirect_to uploader.url
end
unless uploader.file && uploader.file.exists? skip_before_action :authenticate_user!
return render_404 before_action :find_model
end before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
private private
def find_model def find_model
unless upload_model && upload_mount return render_404 unless upload_model && upload_mount
return render_404
end
@model = upload_model.find(params[:id]) @model = upload_model.find(params[:id])
end end
def authorize_access! def authorize_access!
authorized = authorized =
case @model case model
when Project
can?(current_user, :read_project, @model)
when Group
can?(current_user, :read_group, @model)
when Note when Note
can?(current_user, :read_project, @model.project) can?(current_user, :read_project, model.project)
else when User
# No authentication required for user avatars.
true true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
can?(current_user, permission, model)
end end
return if authorized render_unauthorized unless authorized
end
def authorize_create_access!
# for now we support only personal snippets comments
authorized = can?(current_user, :comment_personal_snippet, model)
render_unauthorized unless authorized
end
def render_unauthorized
if current_user if current_user
render_404 render_404
else else
...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController ...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController
"project" => Project, "project" => Project,
"note" => Note, "note" => Note,
"group" => Group, "group" => Group,
"appearance" => Appearance "appearance" => Appearance,
"personal_snippet" => PersonalSnippet
} }
upload_models[params[:model]] upload_models[params[:model]]
end end
def upload_mount def upload_mount
return true unless params[:mounted_as]
upload_mounts = %w(avatar attachment file logo header_logo) upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as]) if upload_mounts.include?(params[:mounted_as])
params[:mounted_as] params[:mounted_as]
end end
end end
def uploader
return @uploader if defined?(@uploader)
if model.is_a?(PersonalSnippet)
@uploader = PersonalFileUploader.new(model, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.send(upload_mount)
redirect_to @uploader.url unless @uploader.file_storage?
end
@uploader
end
def uploader_class
PersonalFileUploader
end
def model
@model ||= find_model
end
end end
...@@ -119,7 +119,9 @@ module BlobHelper ...@@ -119,7 +119,9 @@ module BlobHelper
end end
def blob_raw_url def blob_raw_url
if @snippet if @build && @entry
raw_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: @entry.path)
elsif @snippet
if @snippet.project_id if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet) raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
else else
...@@ -250,6 +252,8 @@ module BlobHelper ...@@ -250,6 +252,8 @@ module BlobHelper
case viewer.blob.external_storage case viewer.blob.external_storage
when :lfs when :lfs
'it is stored in LFS' 'it is stored in LFS'
when :build_artifact
'it is stored as a job artifact'
else else
'it is stored externally' 'it is stored externally'
end end
......
...@@ -9,6 +9,7 @@ module BoardsHelper ...@@ -9,6 +9,7 @@ module BoardsHelper
issue_link_base: namespace_project_issues_path(@project.namespace, @project), issue_link_base: namespace_project_issues_path(@project.namespace, @project),
root_path: root_path, root_path: root_path,
bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project),
default_avatar: image_path(default_avatar)
} }
end end
end end
...@@ -122,6 +122,10 @@ module GitlabRoutingHelper ...@@ -122,6 +122,10 @@ module GitlabRoutingHelper
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end end
def preview_markdown_path(project, *args)
preview_markdown_namespace_project_path(project.namespace, project, *args)
end
def toggle_subscription_path(entity, *args) def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue) if entity.is_a?(Issue)
toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
...@@ -208,6 +212,8 @@ module GitlabRoutingHelper ...@@ -208,6 +212,8 @@ module GitlabRoutingHelper
browse_namespace_project_build_artifacts_path(*args) browse_namespace_project_build_artifacts_path(*args)
when 'file' when 'file'
file_namespace_project_build_artifacts_path(*args) file_namespace_project_build_artifacts_path(*args)
when 'raw'
raw_namespace_project_build_artifacts_path(*args)
end end
end end
......
...@@ -70,6 +70,14 @@ module SortingHelper ...@@ -70,6 +70,14 @@ module SortingHelper
} }
end end
def tags_sort_options_hash
{
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
end
def sort_title_priority def sort_title_priority
'Priority' 'Priority'
end end
......
module Ci
class ArtifactBlob
include BlobLike
attr_reader :entry
def initialize(entry)
@entry = entry
end
delegate :name, :path, to: :entry
def id
Digest::SHA1.hexdigest(path)
end
def size
entry.metadata[:size]
end
def data
"Build artifact #{path}"
end
def mode
entry.metadata[:mode]
end
def external_storage
:build_artifact
end
alias_method :external_size, :size
end
end
...@@ -30,6 +30,7 @@ class Event < ActiveRecord::Base ...@@ -30,6 +30,7 @@ class Event < ActiveRecord::Base
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push?
# Scopes # Scopes
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
...@@ -357,4 +358,9 @@ class Event < ActiveRecord::Base ...@@ -357,4 +358,9 @@ class Event < ActiveRecord::Base
def recent_update? def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end end
def set_last_repository_updated_at
Project.unscoped.where(id: project_id).
update_all(last_repository_updated_at: created_at)
end
end end
...@@ -53,6 +53,11 @@ class Project < ActiveRecord::Base ...@@ -53,6 +53,11 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at) update_column(:last_activity_at, self.created_at)
end end
after_create :set_last_repository_updated_at
def set_last_repository_updated_at
update_column(:last_repository_updated_at, self.created_at)
end
after_destroy :remove_pages after_destroy :remove_pages
# update visibility_level of forks # update visibility_level of forks
......
...@@ -183,6 +183,6 @@ class ProjectWiki ...@@ -183,6 +183,6 @@ class ProjectWiki
end end
def update_project_activity def update_project_activity
@project.touch(:last_activity_at) @project.touch(:last_activity_at, :last_repository_updated_at)
end end
end end
...@@ -23,6 +23,7 @@ class User < ActiveRecord::Base ...@@ -23,6 +23,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_password, false default_value_for :hide_no_password, false
default_value_for :project_view, :files default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
......
...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy
can! :read_personal_snippet if @subject.public? can! :read_personal_snippet if @subject.public?
return unless @user return unless @user
if @subject.public?
can! :comment_personal_snippet
end
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
can! :comment_personal_snippet
end end
unless @user.external? unless @user.external?
...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
can! :comment_personal_snippet
end end
end end
end end
...@@ -48,6 +48,17 @@ module Projects ...@@ -48,6 +48,17 @@ module Projects
available_public_keys.any? available_public_keys.any?
end end
def as_json
serializer = DeployKeySerializer.new
opts = { user: current_user }
{
enabled_keys: serializer.represent(enabled_keys, opts),
available_project_keys: serializer.represent(available_project_keys, opts),
public_keys: serializer.represent(available_public_keys, opts)
}
end
def to_partial_path def to_partial_path
'projects/deploy_keys/index' 'projects/deploy_keys/index'
end end
......
This diff is collapsed.
...@@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity ...@@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity
include EntityDateHelper include EntityDateHelper
expose :title expose :title
expose :name
expose :legend expose :legend
expose :description expose :description
......
class AnalyticsSummaryEntity < Grape::Entity class AnalyticsSummaryEntity < Grape::Entity
expose :value, safe: true expose :value, safe: true
expose :title
expose :title do |object|
object.title.pluralize(object.value)
end
end end
class DeployKeyEntity < Grape::Entity
expose :id
expose :user_id
expose :title
expose :fingerprint
expose :can_push
expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned
expose :almost_orphaned?, as: :almost_orphaned
expose :created_at
expose :updated_at
expose :projects, using: ProjectEntity do |deploy_key|
deploy_key.projects.select { |project| options[:user].can?(:read_project, project) }
end
end
class DeployKeySerializer < BaseSerializer
entity DeployKeyEntity
end
class ProjectEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :name
expose :full_path do |project|
namespace_project_path(project.namespace, project)
end
expose :full_name do |project|
project.full_name
end
end
class PreviewMarkdownService < BaseService
def execute
text, commands = explain_slash_commands(params[:text])
users = find_user_references(text)
success(
text: text,
users: users,
commands: commands.join(' ')
)
end
private
def explain_slash_commands(text)
return text, [] unless %w(Issue MergeRequest).include?(commands_target_type)
slash_commands_service = SlashCommands::InterpretService.new(project, current_user)
slash_commands_service.explain(text, find_commands_target)
end
def find_user_references(text)
extractor = Gitlab::ReferenceExtractor.new(project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
def find_commands_target
if commands_target_id.present?
finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder
finder.new(current_user, project_id: project.id).find(commands_target_id)
else
collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests
collection.build
end
end
def commands_target_type
params[:slash_commands_target_type]
end
def commands_target_id
params[:slash_commands_target_id]
end
end
module Projects
class UploadService < BaseService
def initialize(project, file)
@project, @file = project, file
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment