Commit 9304acc3 authored by Rubén Dávila Santos's avatar Rubén Dávila Santos

Merge branch 'ce-to-ee' into 'master'

CE upstream

Closes #1655, gitlab-ce#8082, gitlab-com/infrastructure#1058, #1333, and #323

See merge request !1185
parents 0150b04f 84a80a98
...@@ -3,4 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb ...@@ -3,4 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb app/controllers/projects/approvers_controller.rb
app/policies/project_policy.rb app/policies/project_policy.rb
\ No newline at end of file
...@@ -432,9 +432,9 @@ pages: ...@@ -432,9 +432,9 @@ pages:
script: script:
- mv public/ .public/ - mv public/ .public/
- mkdir public/ - mkdir public/
- mv coverage public/coverage-ruby - mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/default/ public/coverage-javascript/ - mv coverage-javascript/default/ public/coverage-javascript/ || true
- mv eslint-report.html public/ - mv eslint-report.html public/ || true
artifacts: artifacts:
paths: paths:
- public - public
......
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.16.4 (2017-02-02)
- Support non-ASCII characters in GFM autocomplete. !8729
- Fix search bar search param encoding. !8753
- Fix project name label's for reference in project settings. !8795
- Fix filtering with multiple words. !8830
- Fixed services form cancel not redirecting back the integrations settings view. !8843
- Fix filtering usernames with multiple words. !8851
- Improve performance of slash commands. !8876
- Remove old project members when retrying an export.
- Fix permalink discussion note being collapsed.
- Add project ID index to `project_authorizations` table to optimize queries.
- Check public snippets for spam.
- 19164 Add settings dropdown to mobile screens.
## 8.16.3 (2017-01-27) ## 8.16.3 (2017-01-27)
- Add caching of droplab ajax requests. !8725 - Add caching of droplab ajax requests. !8725
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
- [Issue weight](#issue-weight) - [Issue weight](#issue-weight)
- [Regression issues](#regression-issues) - [Regression issues](#regression-issues)
- [Technical debt](#technical-debt) - [Technical debt](#technical-debt)
- [Stewardship][#stewardship]
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria) - [Contribution acceptance criteria](#contribution-acceptance-criteria)
...@@ -230,6 +231,21 @@ for a release by the appropriate person. ...@@ -230,6 +231,21 @@ for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue. associated with in the description of the issue.
### Stewardship
For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to remove
features from GitLab CE to make exclusive in GitLab EE, related issues
would be labelled with ~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].
[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084
## Merge requests ## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests, We welcome merge requests with fixes and improvements to GitLab code, tests,
......
...@@ -33,7 +33,7 @@ core team members will mention this person. ...@@ -33,7 +33,7 @@ core team members will mention this person.
### 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
their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done]. their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-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/.
...@@ -79,47 +79,47 @@ not be merged into any stable branches. ...@@ -79,47 +79,47 @@ not be merged into any stable branches.
### Improperly formatted issue ### Improperly formatted issue
Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version ### Issue report for old version
Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Support requests and configuration questions ### Support requests and configuration questions
Thanks for your interest in GitLab. We don't use the issue tracker for support Thanks for your interest in GitLab. We don't use the issue tracker for support
requests and configuration questions. Please check our requests and configuration questions. Please check our
\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available [getting help](https://about.gitlab.com/getting-help/) page to see all of the available
support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
for more information. for more information.
### Code format ### Code format
Please use ``` to format console output, logs, and code as it's very hard to read otherwise. Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise.
### Issue fixed in newer version ### Issue fixed in newer version
Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Improperly formatted merge request ### Improperly formatted merge request
Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines). Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
### Inactivity close of an issue ### Inactivity close of an issue
It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Inactivity close of a merge request ### Inactivity close of a merge request
This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request. This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
### Accepting merge requests ### Accepting merge requests
Is there an issue on the Is there an issue on the
\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
similar to this? Could you please link it here? similar to this? Could you please link it here?
Please be aware that new functionality that is not marked Please be aware that new functionality that is not marked
\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) [accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab. might not make it into GitLab.
### Only accepting merge requests with green tests ### Only accepting merge requests with green tests
...@@ -134,7 +134,7 @@ rebase with master to see if that solves the issue. ...@@ -134,7 +134,7 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker. prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is Since this is an older issue I'll be closing this for now. If you think this is
still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues). still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues).
[team]: https://about.gitlab.com/team/ [team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
......
...@@ -252,5 +252,7 @@ window.ES6Promise.polyfill(); ...@@ -252,5 +252,7 @@ window.ES6Promise.polyfill();
new Aside(); new Aside();
// bind sidebar events // bind sidebar events
new gl.Sidebar(); new gl.Sidebar();
gl.utils.initTimeagoTimeout();
}); });
}).call(this); }).call(this);
...@@ -17,7 +17,7 @@ require('./components/boards_selector'); ...@@ -17,7 +17,7 @@ require('./components/boards_selector');
require('./components/board_sidebar'); require('./components/board_sidebar');
require('./components/new_list_dropdown'); require('./components/new_list_dropdown');
require('./components/modal/index'); require('./components/modal/index');
require('./vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
......
/* global Vue */
const userFilter = require('./filters/user');
const milestoneFilter = require('./filters/milestone');
const labelFilter = require('./filters/label');
module.exports = Vue.extend({
name: 'modal-filters',
props: {
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
destroyed() {
gl.issueBoards.ModalStore.setDefaultFilter();
},
components: {
userFilter,
milestoneFilter,
labelFilter,
},
template: `
<div class="modal-filters">
<user-filter
dropdown-class-name="dropdown-menu-author"
toggle-class-name="js-user-search js-author-search"
toggle-label="Author"
field-name="author_id"
:project-id="projectId"></user-filter>
<user-filter
dropdown-class-name="dropdown-menu-author"
toggle-class-name="js-assignee-search"
toggle-label="Assignee"
field-name="assignee_id"
:null-user="true"
:project-id="projectId"></user-filter>
<milestone-filter :milestone-path="milestonePath"></milestone-filter>
<label-filter :label-path="labelPath"></label-filter>
</div>
`,
});
/* eslint-disable no-new */
/* global Vue */
/* global LabelsSelect */
module.exports = Vue.extend({
name: 'filter-label',
props: {
labelPath: {
type: String,
required: true,
},
},
mounted() {
new LabelsSelect(this.$refs.dropdown);
},
template: `
<div class="dropdown">
<button
class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
type="button"
data-toggle="dropdown"
data-show-any="true"
data-show-no="true"
:data-labels="labelPath"
ref="dropdown">
<span class="dropdown-toggle-text">
Label
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
<div class="dropdown-title">
Filter by label
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Search"
autocomplete="off" />
<i class="fa fa-search dropdown-input-search"></i>
<i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`,
});
/* eslint-disable no-new */
/* global Vue */
/* global MilestoneSelect */
module.exports = Vue.extend({
name: 'filter-milestone',
props: {
milestonePath: {
type: String,
required: true,
},
},
mounted() {
new MilestoneSelect(null, this.$refs.dropdown);
},
template: `
<div class="dropdown">
<button
class="dropdown-menu-toggle js-milestone-select"
type="button"
data-toggle="dropdown"
data-show-any="true"
data-show-upcoming="true"
data-field-name="milestone_title"
:data-milestones="milestonePath"
ref="dropdown">
<span class="dropdown-toggle-text">
Milestone
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
<div class="dropdown-title">
<span>Filter by milestone</span>
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Search milestones"
autocomplete="off" />
<i class="fa fa-search dropdown-input-search"></i>
<i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`,
});
/* eslint-disable no-new */
/* global Vue */
/* global UsersSelect */
module.exports = Vue.extend({
name: 'filter-user',
props: {
toggleClassName: {
type: String,
required: true,
},
dropdownClassName: {
type: String,
required: false,
default: '',
},
toggleLabel: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
nullUser: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: true,
},
},
mounted() {
new UsersSelect(null, this.$refs.dropdown);
},
computed: {
currentUsername() {
return gon.current_username;
},
dropdownTitle() {
return `Filter by ${this.toggleLabel.toLowerCase()}`;
},
inputPlaceholder() {
return `Search ${this.toggleLabel.toLowerCase()}`;
},
},
template: `
<div class="dropdown">
<button
class="dropdown-menu-toggle js-user-search"
:class="toggleClassName"
type="button"
data-toggle="dropdown"
data-current-user="true"
:data-any-user="'Any ' + toggleLabel"
:data-null-user="nullUser"
:data-field-name="fieldName"
:data-project-id="projectId"
:data-first-user="currentUsername"
ref="dropdown">
<span class="dropdown-toggle-text">
{{ toggleLabel }}
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div
class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
:class="dropdownClassName">
<div class="dropdown-title">
{{ dropdownTitle }}
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
autocomplete="off"
:placeholder="inputPlaceholder" />
<i class="fa fa-search dropdown-input-search"></i>
<i
role="button"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`,
});
/* global Vue */ /* global Vue */
require('./tabs'); require('./tabs');
const modalFilters = require('./filters');
(() => { (() => {
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
props: {
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() { data() {
return ModalStore.store; return ModalStore.store;
}, },
...@@ -31,6 +45,7 @@ require('./tabs'); ...@@ -31,6 +45,7 @@ require('./tabs');
}, },
components: { components: {
'modal-tabs': gl.issueBoards.ModalTabs, 'modal-tabs': gl.issueBoards.ModalTabs,
modalFilters,
}, },
template: ` template: `
<div> <div>
...@@ -51,6 +66,11 @@ require('./tabs'); ...@@ -51,6 +66,11 @@ require('./tabs');
<div <div
class="add-issues-search append-bottom-10" class="add-issues-search append-bottom-10"
v-if="showSearch"> v-if="showSearch">
<modal-filters
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath">
</modal-filters>
<input <input
placeholder="Search issues..." placeholder="Search issues..."
class="form-control" class="form-control"
......
...@@ -27,6 +27,18 @@ require('./empty_state'); ...@@ -27,6 +27,18 @@ require('./empty_state');
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { data() {
return ModalStore.store; return ModalStore.store;
...@@ -52,17 +64,27 @@ require('./empty_state'); ...@@ -52,17 +64,27 @@ require('./empty_state');
this.issuesCount = false; this.issuesCount = false;
} }
}, },
filter: {
handler() {
this.loadIssues(true);
},
deep: true,
},
}, },
methods: { methods: {
searchOperation: _.debounce(function searchOperationDebounce() { searchOperation: _.debounce(function searchOperationDebounce() {
this.loadIssues(true); this.loadIssues(true);
}, 500), }, 500),
loadIssues(clearIssues = false) { loadIssues(clearIssues = false) {
return gl.boardService.getBacklog({ if (!this.showAddIssuesModal) return false;
const queryData = Object.assign({}, this.filter, {
search: this.searchTerm, search: this.searchTerm,
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
}).then((res) => { });
return gl.boardService.getBacklog(queryData).then((res) => {
const data = res.json(); const data = res.json();
if (clearIssues) { if (clearIssues) {
...@@ -112,8 +134,13 @@ require('./empty_state'); ...@@ -112,8 +134,13 @@ require('./empty_state');
class="add-issues-modal" class="add-issues-modal"
v-if="showAddIssuesModal"> v-if="showAddIssuesModal">
<div class="add-issues-container"> <div class="add-issues-container">
<modal-header></modal-header> <modal-header
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath">
</modal-header>
<modal-list <modal-list
:image="blankStateImage"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
:root-path="rootPath" :root-path="rootPath"
v-if="!loading && showList"></modal-list> v-if="!loading && showList"></modal-list>
......
...@@ -14,6 +14,10 @@ ...@@ -14,6 +14,10 @@
type: String, type: String,
required: true, required: true,
}, },
image: {
type: String,
required: true,
},
}, },
data() { data() {
return ModalStore.store; return ModalStore.store;
...@@ -110,6 +114,19 @@ ...@@ -110,6 +114,19 @@
<section <section
class="add-issues-list add-issues-list-columns" class="add-issues-list add-issues-list-columns"
ref="list"> ref="list">
<div
class="empty-state add-issues-empty-state-filter text-center"
v-if="issuesCount > 0 && issues.length === 0">
<div
class="svg-content"
v-html="image">
</div>
<div class="text-content">
<h4>
There are no issues to show.
</h4>
</div>
</div>
<div <div
v-for="group in groupedIssues" v-for="group in groupedIssues"
class="add-issues-list-column"> class="add-issues-list-column">
......
...@@ -18,6 +18,17 @@ ...@@ -18,6 +18,17 @@
page: 1, page: 1,
perPage: 50, perPage: 50,
}; };
this.setDefaultFilter();
}
setDefaultFilter() {
this.store.filter = {
author_id: '',
assignee_id: '',
milestone_title: '',
label_name: [],
};
} }
selectedCount() { selectedCount() {
......
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
/* global Vue */
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next(function (response) {
Vue.activeResources -= 1;
});
});
/* eslint-disable no-new, no-param-reassign */
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
window.Vue = require('vue');
require('./pipelines_table');
/**
* Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
*
* Renders Pipelines table in pipelines tab in the commits show view.
* Renders Pipelines table in pipelines tab in the merge request show view.
*/
$(() => {
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) {
gl.commits.PipelinesTableBundle.$destroy(true);
}
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
el: document.querySelector('#commit-pipeline-table-view'),
});
});
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
/**
* Pipelines service.
*
* Used to fetch the data used to render the pipelines table.
* Uses Vue.Resource
*/
class PipelinesService {
constructor(endpoint) {
this.pipelines = Vue.resource(endpoint);
}
/**
* Given the root param provided when the class is initialized, will
* make a GET request.
*
* @return {Promise}
*/
all() {
return this.pipelines.get();
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesService = PipelinesService;
/* eslint-disable no-underscore-dangle*/
/**
* Pipelines' Store for commits view.
*
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
class PipelinesStore {
constructor() {
this.state = {};
this.state.pipelines = [];
}
storePipelines(pipelines = []) {
this.state.pipelines = pipelines;
return pipelines;
}
/**
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(function timeloopInterval() {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
/* eslint-disable no-new, no-param-reassign */
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service');
require('./pipelines_store');
/**
*
* Uses `pipelines-table-component` to render Pipelines table with an API call.
* Endpoint is provided in HTML and passed as `endpoint`.
* We need a store to store the received environemnts.
* We need a service to communicate with the server.
*
* Necessary SVG in the table are provided as props. This should be refactored
* as soon as we have Webpack and can load them directly into JS files.
*/
(() => {
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
components: {
'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
},
/**
* Accesses the DOM to provide the needed data.
* Returns the necessary props to render `pipelines-table-component` component.
*
* @return {Object}
*/
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset;
const store = new gl.commits.pipelines.PipelinesStore();
// Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
return {
endpoint: pipelinesTableData.endpoint,
svgs: svgsObject,
store,
state: store.state,
isLoading: false,
};
},
/**
* When the component is created the service to fetch the data will be
* initialized with the correct endpoint.
*
* A request to fetch the pipelines will be made.
* In case of a successfull response we will store the data in the provided
* store, in case of a failed response we need to warn the user.
*
*/
created() {
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
this.isLoading = true;
return pipelinesService.all()
.then(response => response.json())
.then((json) => {
this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
});
},
template: `
<div>
<div class="pipelines realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.pipelines.length === 0">
<h2 class="blank-state-title js-blank-state-title">
No pipelines to show
</h2>
</div>
<div class="table-holder pipelines"
v-if="!isLoading && state.pipelines.length > 0">
<pipelines-table-component
:pipelines="state.pipelines"
:svgs="svgs">
</pipelines-table-component>
</div>
</div>
`,
});
})();
...@@ -166,7 +166,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -166,7 +166,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:commit:pipelines': case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({ new gl.MiniPipelineGraph({
container: '.js-pipeline-table', container: '.js-pipeline-table',
}); }).bindEvents();
break; break;
case 'projects:commits:show': case 'projects:commits:show':
case 'projects:activity': case 'projects:activity':
...@@ -275,7 +275,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -275,7 +275,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:variables:index': case 'projects:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
break; break;
case 'ci:lints:create': case 'ci:lints:create':
......
...@@ -78,8 +78,8 @@ require('../window')(function(w){ ...@@ -78,8 +78,8 @@ require('../window')(function(w){
}, },
destroy: function() { destroy: function() {
if (this.listTemplate) { var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate; dynamicList.outerHTML = this.listTemplate;
} }
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
window.Vue = require('vue'); window.Vue = require('vue');
window.timeago = require('vendor/timeago'); window.timeago = require('vendor/timeago');
require('../../lib/utils/text_utility'); require('../../lib/utils/text_utility');
require('../../vue_common_component/commit'); require('../../vue_shared/components/commit');
require('./environment_actions'); require('./environment_actions');
require('./environment_external_url'); require('./environment_external_url');
require('./environment_stop'); require('./environment_stop');
...@@ -147,12 +147,12 @@ require('./environment_terminal_button'); ...@@ -147,12 +147,12 @@ require('./environment_terminal_button');
}, },
/** /**
* Returns the value of the `stoppable?` key provided in the response. * Returns the value of the `stop_action?` key provided in the response.
* *
* @returns {Boolean} * @returns {Boolean}
*/ */
isStoppable() { hasStopAction() {
return this.model['stoppable?']; return this.model['stop_action?'];
}, },
/** /**
...@@ -508,7 +508,7 @@ require('./environment_terminal_button'); ...@@ -508,7 +508,7 @@ require('./environment_terminal_button');
</external-url-component> </external-url-component>
</div> </div>
<div v-if="isStoppable && canCreateDeployment" <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container"> class="inline js-stop-component-container">
<stop-component <stop-component
:stop-url="model.stop_path"> :stop-url="model.stop_path">
......
window.Vue = require('vue'); window.Vue = require('vue');
require('./stores/environments_store'); require('./stores/environments_store');
require('./components/environment'); require('./components/environment');
require('./vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -8,7 +8,7 @@ require('./filtered_search_dropdown'); ...@@ -8,7 +8,7 @@ require('./filtered_search_dropdown');
super(droplab, dropdown, input, filter); super(droplab, dropdown, input, filter);
this.config = { this.config = {
droplabAjaxFilter: { droplabAjaxFilter: {
endpoint: '/autocomplete/users.json', endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search', searchKey: 'search',
params: { params: {
per_page: 20, per_page: 20,
......
...@@ -4,10 +4,17 @@ ...@@ -4,10 +4,17 @@
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
function LabelsSelect() { function LabelsSelect(els) {
var _this; var _this, $els;
_this = this; _this = this;
$('.js-label-select').each(function(i, dropdown) {
$els = $(els);
if (!els) {
$els = $('.js-label-select');
}
$els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown); $dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter'); $dropdownContainer = $dropdown.closest('.labels-filter');
...@@ -324,7 +331,7 @@ ...@@ -324,7 +331,7 @@
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) { clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page; var isIssueIndex, isMRIndex, page, boardsModel;
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
...@@ -346,22 +353,31 @@ ...@@ -346,22 +353,31 @@
return; return;
} }
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
!$dropdown.closest('.add-issues-modal').length) {
boardsModel = gl.issueBoards.BoardsStore.state.filters;
} else if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = gl.issueBoards.ModalStore.store.filter;
}
if (boardsModel) {
if (label.isAny) { if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = []; boardsModel['label_name'] = [];
} }
else if ($el.hasClass('is-active')) { else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); boardsModel['label_name'].push(label.title);
} }
else { else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; var filters = boardsModel['label_name'];
filters = filters.filter(function (filteredLabel) { filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title; return filteredLabel !== label.title;
}); });
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters; boardsModel['label_name'] = filters;
} }
gl.issueBoards.BoardsStore.updateFiltersUrl(); if (!$dropdown.closest('.add-issues-modal').length) {
gl.issueBoards.BoardsStore.updateFiltersUrl();
}
e.preventDefault(); e.preventDefault();
return; return;
} }
......
...@@ -7,19 +7,28 @@ ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort. ...@@ -7,19 +7,28 @@ ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort.
File.basename(file, '.js').sub(/^mode-/, '') File.basename(file, '.js').sub(/^mode-/, '')
end end
%> %>
// Lazy-load configuration when ace.edit is called
(function() { (function() {
window.gon = window.gon || {}; var basePath;
var basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace'; var ace = window.ace;
ace.config.set('basePath', basePath); var edit = ace.edit;
ace.edit = function() {
window.gon = window.gon || {};
basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
ace.config.set('basePath', basePath);
// configure paths for all worker modules // configure paths for all worker modules
<% ace_workers.each do |worker| %> <% ace_workers.each do |worker| %>
ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/worker-<%= worker %>.js'); ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= File.basename(asset_path("ace/worker-#{worker}.js")) %>');
<% end %> <% end %>
// configure paths for all mode modules // configure paths for all mode modules
<% ace_modes.each do |mode| %> <% ace_modes.each do |mode| %>
ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/mode-<%= mode %>.js'); ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= File.basename(asset_path("ace/mode-#{mode}.js")) %>');
<% end %> <% end %>
// restore original method
ace.edit = edit;
return ace.edit.apply(ace, arguments);
};
})(); })();
...@@ -69,6 +69,9 @@ ...@@ -69,6 +69,9 @@
var hash = w.gl.utils.getLocationHash(); var hash = w.gl.utils.getLocationHash();
if (!hash) return; if (!hash) return;
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
var navbar = document.querySelector('.navbar-gitlab'); var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav'); var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix'); var fixedTabs = document.querySelector('.js-tabs-affix');
...@@ -134,6 +137,14 @@ ...@@ -134,6 +137,14 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
}; };
gl.utils.isMetaClick = function(e) {
// Identify following special clicks
// 1) Cmd + Click on Mac (e.metaKey)
// 2) Ctrl + Click on PC (e.ctrlKey)
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
return e.metaKey || e.ctrlKey || e.which === 2;
};
gl.utils.scrollToElement = function($el) { gl.utils.scrollToElement = function($el) {
var top = $el.offset().top; var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
...@@ -230,5 +241,16 @@ ...@@ -230,5 +241,16 @@
return upperCaseHeaders; return upperCaseHeaders;
}; };
/**
* Transforms a DOMStringMap into a plain object.
*
* @param {DOMStringMap} DOMStringMapObject
* @returns {Object}
*/
w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
acc[element] = DOMStringMapObject[element];
return acc;
}, {});
})(window); })(window);
}).call(this); }).call(this);
...@@ -8,6 +8,8 @@ window.dateFormat = require('vendor/date.format'); ...@@ -8,6 +8,8 @@ window.dateFormat = require('vendor/date.format');
(function() { (function() {
(function(w) { (function(w) {
var base; var base;
var timeagoInstance;
if (w.gl == null) { if (w.gl == null) {
w.gl = {}; w.gl = {};
} }
...@@ -24,49 +26,51 @@ window.dateFormat = require('vendor/date.format'); ...@@ -24,49 +26,51 @@ window.dateFormat = require('vendor/date.format');
return this.days[date.getDay()]; return this.days[date.getDay()];
}; };
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
if (setTimeago == null) { $timeagoEls.each((i, el) => {
setTimeago = true; el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
}
$timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
if (setTimeago) { if (setTimeago) {
// Recreate with custom template // Recreate with custom template
$el.tooltip({ $(el).tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
}); });
} }
$el.attr('data-timeago-rendered', true); el.classList.add('js-timeago-render');
gl.utils.renderTimeago($el);
}); });
gl.utils.renderTimeago($timeagoEls);
}; };
w.gl.utils.getTimeago = function() { w.gl.utils.getTimeago = function() {
var locale = function(number, index) { var locale;
return [
['less than a minute ago', 'a while'], if (!timeagoInstance) {
['less than a minute ago', 'in %s seconds'], locale = function(number, index) {
['about a minute ago', 'in 1 minute'], return [
['%s minutes ago', 'in %s minutes'], ['less than a minute ago', 'a while'],
['about an hour ago', 'in 1 hour'], ['less than a minute ago', 'in %s seconds'],
['about %s hours ago', 'in %s hours'], ['about a minute ago', 'in 1 minute'],
['a day ago', 'in 1 day'], ['%s minutes ago', 'in %s minutes'],
['%s days ago', 'in %s days'], ['about an hour ago', 'in 1 hour'],
['a week ago', 'in 1 week'], ['about %s hours ago', 'in %s hours'],
['%s weeks ago', 'in %s weeks'], ['a day ago', 'in 1 day'],
['a month ago', 'in 1 month'], ['%s days ago', 'in %s days'],
['%s months ago', 'in %s months'], ['a week ago', 'in 1 week'],
['a year ago', 'in 1 year'], ['%s weeks ago', 'in %s weeks'],
['%s years ago', 'in %s years'] ['a month ago', 'in 1 month'],
][index]; ['%s months ago', 'in %s months'],
}; ['a year ago', 'in 1 year'],
['%s years ago', 'in %s years']
timeago.register('gl_en', locale); ][index];
return timeago(); };
timeago.register('gl_en', locale);
timeagoInstance = timeago();
}
return timeagoInstance;
}; };
w.gl.utils.timeFor = function(time, suffix, expiredLabel) { w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
...@@ -85,9 +89,30 @@ window.dateFormat = require('vendor/date.format'); ...@@ -85,9 +89,30 @@ window.dateFormat = require('vendor/date.format');
return timefor; return timefor;
}; };
w.gl.utils.renderTimeago = function($element) { w.gl.utils.cachedTimeagoElements = [];
var timeagoInstance = gl.utils.getTimeago(); w.gl.utils.renderTimeago = function($els) {
timeagoInstance.render($element, 'gl_en'); if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const timeago = gl.utils.getTimeago();
const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
}; };
w.gl.utils.getDayDifference = function(a, b) { w.gl.utils.getDayDifference = function(a, b) {
......
...@@ -61,7 +61,6 @@ require('./flash'); ...@@ -61,7 +61,6 @@ require('./flash');
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false; this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
this.fixedLayoutPref = null; this.fixedLayoutPref = null;
...@@ -83,12 +82,18 @@ require('./flash'); ...@@ -83,12 +82,18 @@ require('./flash');
$(document) $(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab); .on('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.on('click', this.clickTab);
} }
unbindEvents() { unbindEvents() {
$(document) $(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab); .off('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.off('click', this.clickTab);
} }
showTab(e) { showTab(e) {
...@@ -96,6 +101,14 @@ require('./flash'); ...@@ -96,6 +101,14 @@ require('./flash');
this.activateTab($(e.target).data('action')); this.activateTab($(e.target).data('action'));
} }
clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href');
e.stopImmediatePropagation();
window.open(targetLink, '_blank');
}
}
tabShown(e) { tabShown(e) {
const $target = $(e.target); const $target = $(e.target);
const action = $target.data('action'); const action = $target.data('action');
...@@ -116,10 +129,6 @@ require('./flash'); ...@@ -116,10 +129,6 @@ require('./flash');
$.scrollTo('.merge-request-details .merge-request-tabs', { $.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight, offset: -navBarHeight,
}); });
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
...@@ -244,25 +253,6 @@ require('./flash'); ...@@ -244,25 +253,6 @@ require('./flash');
}); });
} }
loadPipelines(source) {
if (this.pipelinesLoaded) {
return;
}
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
$('#pipelines').html(data.html);
gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
this.pipelinesLoaded = true;
this.scrollToElement('#pipelines');
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
},
});
}
// Show or hide the loading spinner // Show or hide the loading spinner
// //
// status - Boolean, true to show, false to hide // status - Boolean, true to show, false to hide
......
...@@ -50,6 +50,8 @@ require('./smart_interval'); ...@@ -50,6 +50,8 @@ require('./smart_interval');
this.getCIStatus(false); this.getCIStatus(false);
this.retrieveSuccessIcon(); this.retrieveSuccessIcon();
this.initMiniPipelineGraph();
this.ciStatusInterval = new global.SmartInterval({ this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true), callback: this.getCIStatus.bind(this, true),
startingInterval: 10000, startingInterval: 10000,
...@@ -65,6 +67,7 @@ require('./smart_interval'); ...@@ -65,6 +67,7 @@ require('./smart_interval');
incrementByFactorOf: 15000, incrementByFactorOf: 15000,
immediateExecution: true, immediateExecution: true,
}); });
notifyPermissions(); notifyPermissions();
} }
...@@ -253,17 +256,20 @@ require('./smart_interval'); ...@@ -253,17 +256,20 @@ require('./smart_interval');
case "failed": case "failed":
case "canceled": case "canceled":
case "not_found": case "not_found":
return this.setMergeButtonClass('btn-danger'); this.setMergeButtonClass('btn-danger');
break;
case "running": case "running":
return this.setMergeButtonClass('btn-info'); this.setMergeButtonClass('btn-info');
break;
case "success": case "success":
case "success_with_warnings": case "success_with_warnings":
return this.setMergeButtonClass('btn-create'); this.setMergeButtonClass('btn-create');
} }
} else { } else {
$('.ci_widget.ci-error').show(); $('.ci_widget.ci-error').show();
return this.setMergeButtonClass('btn-danger'); this.setMergeButtonClass('btn-danger');
} }
this.initMiniPipelineGraph();
}; };
MergeRequestWidget.prototype.showCICoverage = function(coverage) { MergeRequestWidget.prototype.showCICoverage = function(coverage) {
...@@ -286,6 +292,12 @@ require('./smart_interval'); ...@@ -286,6 +292,12 @@ require('./smart_interval');
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
}; };
MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
new gl.MiniPipelineGraph({
container: '.js-pipeline-inline-mr-widget-graph:visible',
}).bindEvents();
};
return MergeRequestWidget; return MergeRequestWidget;
})(); })();
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -5,13 +5,20 @@ ...@@ -5,13 +5,20 @@
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) { function MilestoneSelect(currentProject, els) {
var _this; var _this, $els;
if (currentProject != null) { if (currentProject != null) {
_this = this; _this = this;
this.currentProject = JSON.parse(currentProject); this.currentProject = JSON.parse(currentProject);
} }
$('.js-milestone-select').each(function(i, dropdown) {
$els = $(els);
if (!els) {
$els = $('.js-milestone-select');
}
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); projectId = $dropdown.data('project-id');
...@@ -108,7 +115,7 @@ ...@@ -108,7 +115,7 @@
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) { clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page; var data, isIssueIndex, isMRIndex, page, boardsStore;
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
...@@ -116,9 +123,19 @@ ...@@ -116,9 +123,19 @@
e.preventDefault(); e.preventDefault();
return; return;
} }
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
gl.issueBoards.BoardsStore.updateFiltersUrl(); !$dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.BoardsStore.state.filters;
} else if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
if (!$dropdown.closest('.add-issues-modal').length) {
gl.issueBoards.BoardsStore.updateFiltersUrl();
}
e.preventDefault(); e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) { if (selected.name != null) {
......
...@@ -21,8 +21,6 @@ ...@@ -21,8 +21,6 @@
this.container = opts.container || ''; this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container'; this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this); this.getBuildsList = this.getBuildsList.bind(this);
this.bindEvents();
} }
/** /**
......
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */ /* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */ /* global Cookies */
((global) => { (() => {
let singleton;
const pinnedStateCookie = 'pin_nav'; const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024; const sidebarBreakpoint = 1024;
...@@ -23,11 +21,12 @@ ...@@ -23,11 +21,12 @@
class Sidebar { class Sidebar {
constructor() { constructor() {
if (!singleton) { if (!Sidebar.singleton) {
singleton = this; Sidebar.singleton = this;
singleton.init(); Sidebar.singleton.init();
} }
return singleton;
return Sidebar.singleton;
} }
init() { init() {
...@@ -39,7 +38,7 @@ ...@@ -39,7 +38,7 @@
$(document) $(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body', (e) => this.handleClickEvent(e)) .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState()) .on('DOMContentLoaded', () => this.renderState())
.on('todo:toggle', (e, count) => this.updateTodoCount(count)); .on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState(); this.renderState();
...@@ -88,10 +87,12 @@ ...@@ -88,10 +87,12 @@
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState); $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) { if (this.isExpanded) {
setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200); const sidebarContent = $(sidebarContentSelector);
setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
} }
} }
} }
global.Sidebar = Sidebar; window.gl = window.gl || {};
})(window.gl || (window.gl = {})); gl.Sidebar = Sidebar;
})();
...@@ -146,14 +146,26 @@ ...@@ -146,14 +146,26 @@
} }
goToTodoUrl(e) { goToTodoUrl(e) {
const todoLink = $(this).data('url'); const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) { if (!todoLink) {
return; return;
} }
// Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) { if (gl.utils.isMetaClick(e)) {
e.preventDefault(); e.preventDefault();
return window.open(todoLink, '_blank'); // Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually.
if (targetLink !== todoLink) {
return window.open(targetLink, '_blank');
} else {
return window.open(todoLink, '_blank');
}
} else { } else {
return gl.utils.visitUrl(todoLink); return gl.utils.visitUrl(todoLink);
} }
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
slice = [].slice; slice = [].slice;
this.UsersSelect = (function() { this.UsersSelect = (function() {
function UsersSelect(currentUser) { function UsersSelect(currentUser, els) {
var $els;
this.users = bind(this.users, this); this.users = bind(this.users, this);
this.user = bind(this.user, this); this.user = bind(this.user, this);
this.usersPath = "/autocomplete/users.json"; this.usersPath = "/autocomplete/users.json";
...@@ -20,7 +21,14 @@ ...@@ -20,7 +21,14 @@
this.currentUser = JSON.parse(currentUser); this.currentUser = JSON.parse(currentUser);
} }
} }
$('.js-user-search').each((function(_this) {
$els = $(els);
if (!els) {
$els = $('.js-user-search');
}
$els.each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
...@@ -193,7 +201,9 @@ ...@@ -193,7 +201,9 @@
selectedId = user.id; selectedId = user.id;
return; return;
} }
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id; selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl(); gl.issueBoards.BoardsStore.updateFiltersUrl();
......
/* eslint-disable no-param-reassign */
/* global Vue, VueResource, gl */ /* global Vue, VueResource, gl */
window.Vue = require('vue'); window.Vue = require('vue');
window.Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
require('../vue_common_component/commit'); require('../lib/utils/common_utils');
require('../vue_pagination/index'); require('../vue_shared/vue_resource_interceptor');
require('../boards/vue_resource_interceptor');
require('./status');
require('./store');
require('./pipeline_url');
require('./stage');
require('./stages');
require('./pipeline_actions');
require('./time_ago');
require('./pipelines'); require('./pipelines');
(() => { $(() => new Vue({
const project = document.querySelector('.pipelines'); el: document.querySelector('.vue-pipelines-index'),
const entry = document.querySelector('.vue-pipelines-index');
const svgs = document.querySelector('.pipeline-svgs');
if (!entry) return null; data() {
return new Vue({ const project = document.querySelector('.pipelines');
el: entry, const svgs = document.querySelector('.pipeline-svgs').dataset;
data: {
// Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgs);
return {
scope: project.dataset.url, scope: project.dataset.url,
store: new gl.PipelineStore(), store: new gl.PipelineStore(),
svgs: svgs.dataset, svgs: svgsObject,
}, };
components: { },
'vue-pipelines': gl.VuePipelines, components: {
}, 'vue-pipelines': gl.VuePipelines,
template: ` },
<vue-pipelines template: `
:scope='scope' <vue-pipelines
:store='store' :scope='scope'
:svgs='svgs' :store='store'
> :svgs='svgs'
</vue-pipelines> >
`, </vue-pipelines>
}); `,
})(); }));
...@@ -50,9 +50,9 @@ ...@@ -50,9 +50,9 @@
<button <button
v-if='artifacts' v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
data-toggle="dropdown"
title="Artifacts" title="Artifacts"
data-placement="top" data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts" aria-label="Artifacts"
> >
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
...@@ -81,8 +81,7 @@ ...@@ -81,8 +81,7 @@
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
:href='pipeline.retry_path' :href='pipeline.retry_path'
aria-label="Retry" aria-label="Retry">
>
<i class="fa fa-repeat" aria-hidden="true"></i> <i class="fa fa-repeat" aria-hidden="true"></i>
</a> </a>
<a <a
...@@ -94,8 +93,7 @@ ...@@ -94,8 +93,7 @@
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
:href='pipeline.cancel_path' :href='pipeline.cancel_path'
aria-label="Cancel" aria-label="Cancel">
>
<i class="fa fa-remove" aria-hidden="true"></i> <i class="fa fa-remove" aria-hidden="true"></i>
</a> </a>
</div> </div>
......
/* global Vue, gl */ /* global Vue, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
window.Vue = require('vue');
require('../vue_shared/components/table_pagination');
require('./store');
require('../vue_shared/components/pipelines_table');
((gl) => { ((gl) => {
gl.VuePipelines = Vue.extend({ gl.VuePipelines = Vue.extend({
components: { components: {
runningPipeline: gl.VueRunningPipeline, 'gl-pagination': gl.VueGlPagination,
pipelineActions: gl.VuePipelineActions, 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
stages: gl.VueStages,
commit: gl.CommitComponent,
pipelineUrl: gl.VuePipelineUrl,
pipelineHead: gl.VuePipelineHead,
glPagination: gl.VueGlPagination,
statusScope: gl.VueStatusScope,
timeAgo: gl.VueTimeAgo,
}, },
data() { data() {
return { return {
pipelines: [], pipelines: [],
...@@ -38,87 +38,29 @@ ...@@ -38,87 +38,29 @@
change(pagenum, apiScope) { change(pagenum, apiScope) {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
}, },
author(pipeline) {
if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
if (pipeline.commit.author) return pipeline.commit.author;
return {
avatar_url: pipeline.commit.author_gravatar_url,
web_url: `mailto:${pipeline.commit.author_email}`,
username: pipeline.commit.author_name,
};
},
ref(pipeline) {
const { ref } = pipeline;
return { name: ref.name, tag: ref.tag, ref_url: ref.path };
},
commitTitle(pipeline) {
return pipeline.commit ? pipeline.commit.title : '';
},
commitSha(pipeline) {
return pipeline.commit ? pipeline.commit.short_id : '';
},
commitUrl(pipeline) {
return pipeline.commit ? pipeline.commit.commit_path : '';
},
match(string) {
return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
},
}, },
template: ` template: `
<div> <div>
<div class="pipelines realtime-loading" v-if='pipelines.length < 1'> <div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
</div> </div>
<div class="table-holder" v-if='pipelines.length'>
<table class="table ci-table"> <div class="blank-state blank-state-no-icon"
<thead> v-if="!pageRequest && pipelines.length === 0">
<tr> <h2 class="blank-state-title js-blank-state-title">
<th class="pipeline-status">Status</th> No pipelines to show
<th class="pipeline-info">Pipeline</th> </h2>
<th class="pipeline-commit">Commit</th>
<th class="pipeline-stages">Stages</th>
<th class="pipeline-date"></th>
<th class="pipeline-actions hidden-xs"></th>
</tr>
</thead>
<tbody>
<tr class="commit" v-for='pipeline in pipelines'>
<status-scope
:pipeline='pipeline'
:match='match'
:svgs='svgs'
>
</status-scope>
<pipeline-url :pipeline='pipeline'></pipeline-url>
<td>
<commit
:commit-icon-svg='svgs.commitIconSvg'
:author='author(pipeline)'
:tag="pipeline.ref.tag"
:title='commitTitle(pipeline)'
:commit-ref='ref(pipeline)'
:short-sha='commitSha(pipeline)'
:commit-url='commitUrl(pipeline)'
>
</commit>
</td>
<stages
:pipeline='pipeline'
:svgs='svgs'
:match='match'
>
</stages>
<time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
<pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
</tr>
</tbody>
</table>
</div> </div>
<div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i> <div class="table-holder" v-if='!pageRequest && pipelines.length'>
<pipelines-table-component
:pipelines='pipelines'
:svgs='svgs'>
</pipelines-table-component>
</div> </div>
<gl-pagination <gl-pagination
v-if='pageInfo.total > pageInfo.perPage' v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
:pagenum='pagenum' :pagenum='pagenum'
:change='change' :change='change'
:count='count.all' :count='count.all'
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
required: true, required: true,
}, },
svgs: { svgs: {
type: DOMStringMap, type: Object,
required: true, required: true,
}, },
match: { match: {
......
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStages = Vue.extend({
components: {
'vue-stage': gl.VueStage,
},
props: ['pipeline', 'svgs', 'match'],
template: `
<td class="stage-cell">
<div
class="stage-container dropdown js-mini-pipeline-graph"
v-for='stage in pipeline.details.stages'
>
<vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
...@@ -20,6 +20,7 @@ require('../vue_realtime_listener'); ...@@ -20,6 +20,7 @@ require('../vue_realtime_listener');
gl.PipelineStore = class { gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) { fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true;
const updatePipelineNums = (count) => { const updatePipelineNums = (count) => {
const { all } = count; const { all } = count;
const running = count.running_or_pending; const running = count.running_or_pending;
...@@ -41,16 +42,18 @@ require('../vue_realtime_listener'); ...@@ -41,16 +42,18 @@ require('../vue_realtime_listener');
this.pageRequest = false; this.pageRequest = false;
}, () => { }, () => {
this.pageRequest = false; this.pageRequest = false;
return new Flash('Something went wrong on our end.'); return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
}); });
goFetch(); goFetch();
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => { this.timeLoopInterval = setInterval(() => {
this.$children this.$children[0].$children.reduce((acc, component) => {
.filter(e => e.$options._componentTag === 'time-ago') const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
.forEach(e => e.changeTime()); acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000); }, 10000);
}; };
......
/* global Vue, gl */ /* global Vue, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
window.Vue = require('vue');
require('../lib/utils/datetime_utility');
((gl) => { ((gl) => {
gl.VueTimeAgo = Vue.extend({ gl.VueTimeAgo = Vue.extend({
data() { data() {
......
/* global Vue */ /* global Vue */
window.Vue = require('vue');
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
/* eslint-disable no-param-reassign */
/* global Vue */
require('./pipelines_table_row');
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
(() => {
window.gl = window.gl || {};
gl.pipelines = gl.pipelines || {};
gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
props: {
pipelines: {
type: Array,
required: true,
default: () => ([]),
},
/**
* TODO: Remove this when we have webpack.
*/
svgs: {
type: Object,
required: true,
default: () => ({}),
},
},
components: {
'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
},
template: `
<table class="table ci-table">
<thead>
<tr>
<th class="js-pipeline-status pipeline-status">Status</th>
<th class="js-pipeline-info pipeline-info">Pipeline</th>
<th class="js-pipeline-commit pipeline-commit">Commit</th>
<th class="js-pipeline-stages pipeline-stages">Stages</th>
<th class="js-pipeline-date pipeline-date"></th>
<th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
</tr>
</thead>
<tbody>
<template v-for="model in pipelines"
v-bind:model="model">
<tr is="pipelines-table-row-component"
:pipeline="model"
:svgs="svgs"></tr>
</template>
</tbody>
</table>
`,
});
})();
/* eslint-disable no-param-reassign */
/* global Vue */
require('../../vue_pipelines_index/status');
require('../../vue_pipelines_index/pipeline_url');
require('../../vue_pipelines_index/stage');
require('../../vue_pipelines_index/pipeline_actions');
require('../../vue_pipelines_index/time_ago');
require('./commit');
/**
* Pipeline table row.
*
* Given the received object renders a table row in the pipelines' table.
*/
(() => {
window.gl = window.gl || {};
gl.pipelines = gl.pipelines || {};
gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
props: {
pipeline: {
type: Object,
required: true,
default: () => ({}),
},
/**
* TODO: Remove this when we have webpack;
*/
svgs: {
type: Object,
required: true,
default: () => ({}),
},
},
components: {
'commit-component': gl.CommitComponent,
'pipeline-actions': gl.VuePipelineActions,
'dropdown-stage': gl.VueStage,
'pipeline-url': gl.VuePipelineUrl,
'status-scope': gl.VueStatusScope,
'time-ago': gl.VueTimeAgo,
},
computed: {
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
*
* This field needs a lot of verification, because of different possible cases:
*
* 1. person who is an author of a commit might be a GitLab user
* 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
* 3. If GitLab user does not have avatar he/she might have a Gravatar
* 4. If committer is not a GitLab User he/she can have a Gravatar
* 5. We do not have consistent API object in this case
* 6. We should improve API and the code
*
* @returns {Object|Undefined}
*/
commitAuthor() {
let commitAuthorInformation;
// 1. person who is an author of a commit might be a GitLab user
if (this.pipeline &&
this.pipeline.commit &&
this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// he/she can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar he/she might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
avatar_url: this.pipeline.commit.author_gravatar_url,
});
}
}
// 4. If committer is not a GitLab User he/she can have a Gravatar
if (this.pipeline &&
this.pipeline.commit) {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
web_url: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation;
},
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.pipeline.ref &&
this.pipeline.ref.tag) {
return this.pipeline.ref.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
* Needed to render the commit component column.
*
* Matched `url` prop sent in the API to `path` prop needed
* in the commit component.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'url') {
accumulator.path = this.pipeline.ref[prop];
} else {
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
}, {});
}
return undefined;
},
/**
* If provided, returns the commit url.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.pipeline.commit &&
this.pipeline.commit.commit_path) {
return this.pipeline.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.pipeline.commit &&
this.pipeline.commit.short_id) {
return this.pipeline.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.pipeline.commit &&
this.pipeline.commit.title) {
return this.pipeline.commit.title;
}
return undefined;
},
},
methods: {
/**
* FIXME: This should not be in this component but in the components that
* need this function.
*
* Used to render SVGs in the following components:
* - status-scope
* - dropdown-stage
*
* @param {String} string
* @return {String}
*/
match(string) {
return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
},
},
template: `
<tr class="commit">
<status-scope
:pipeline="pipeline"
:svgs="svgs"
:match="match">
</status-scope>
<pipeline-url :pipeline="pipeline"></pipeline-url>
<td>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:commit-icon-svg="svgs.commitIconSvg">
</commit-component>
</td>
<td class="stage-cell">
<div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages">
<dropdown-stage
:stage="stage"
:svgs="svgs"
:match="match">
</dropdown-stage>
</div>
</td>
<time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
<pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
</tr>
`,
});
})();
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
no-param-reassign, no-plusplus */
/* global Vue */ /* global Vue */
Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => { next((response) => {
if (typeof response.data === 'string') { if (typeof response.data === 'string') {
response.data = JSON.parse(response.data); // eslint-disable-line response.data = JSON.parse(response.data);
} }
Vue.activeResources--; // eslint-disable-line Vue.activeResources--;
}); });
}); });
Vue.http.interceptors.push((request, next) => {
// needed in order to not break the tests.
if ($.rails) {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
});
...@@ -71,6 +71,27 @@ ...@@ -71,6 +71,27 @@
transition: $unfoldedTransitions; transition: $unfoldedTransitions;
} }
@mixin disableAllAnimation {
/*CSS transitions*/
-o-transition-property: none !important;
-moz-transition-property: none !important;
-ms-transition-property: none !important;
-webkit-transition-property: none !important;
transition-property: none !important;
/*CSS transforms*/
-o-transform: none !important;
-moz-transform: none !important;
-ms-transform: none !important;
-webkit-transform: none !important;
transform: none !important;
/*CSS animations*/
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
-ms-animation: none !important;
animation: none !important;
}
@function unfoldTransition ($transition) { @function unfoldTransition ($transition) {
// Default values // Default values
$property: all; $property: all;
...@@ -116,11 +137,13 @@ a { ...@@ -116,11 +137,13 @@ a {
@include transition(background-color, color, border); @include transition(background-color, color, border);
} }
.tree-table td,
.well-list > li {
@include transition(background-color, border-color);
}
.stage-nav-item { .stage-nav-item {
@include transition(background-color, box-shadow); @include transition(background-color, box-shadow);
} }
.nav-sidebar a,
.dropdown-menu a,
.dropdown-menu button,
.dropdown-menu-nav a {
transition: none;
}
...@@ -253,6 +253,8 @@ li.note { ...@@ -253,6 +253,8 @@ li.note {
.progress { .progress {
margin-bottom: 0; margin-bottom: 0;
margin-top: 4px; margin-top: 4px;
box-shadow: none;
background-color: $border-gray-light;
} }
} }
......
...@@ -313,3 +313,7 @@ ul.controls { ...@@ -313,3 +313,7 @@ ul.controls {
} }
} }
} }
ul.indent-list {
padding: 10px 0 0 30px;
}
...@@ -159,6 +159,7 @@ ...@@ -159,6 +159,7 @@
.cur { .cur {
.avatar { .avatar {
border: 1px solid $white-light; border: 1px solid $white-light;
@include disableAllAnimation;
} }
} }
} }
...@@ -6,8 +6,22 @@ ...@@ -6,8 +6,22 @@
.pagination { .pagination {
padding: 0; padding: 0;
a {
cursor: pointer;
}
.separator,
.separator:hover {
a {
cursor: default;
background-color: $gray-light;
padding: $gl-vert-padding;
}
}
} }
.gap, .gap,
.gap:hover { .gap:hover {
background-color: $gray-light; background-color: $gray-light;
......
...@@ -423,6 +423,13 @@ ...@@ -423,6 +423,13 @@
flex: 1; flex: 1;
margin-top: 0; margin-top: 0;
&.add-issues-empty-state-filter {
-webkit-flex-direction: column;
flex-direction: column;
-webkit-justify-content: center;
justify-content: center;
}
> .row { > .row {
width: 100%; width: 100%;
margin: auto 0; margin: auto 0;
...@@ -450,6 +457,14 @@ ...@@ -450,6 +457,14 @@
.add-issues-search { .add-issues-search {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
.form-control {
margin-left: auto;
@media (min-width: $screen-sm-min) {
max-width: 200px;
}
}
} }
.add-issues-list-column { .add-issues-list-column {
...@@ -520,3 +535,24 @@ ...@@ -520,3 +535,24 @@
line-height: 15px; line-height: 15px;
border-radius: 50%; border-radius: 50%;
} }
.modal-filters {
display: flex;
> .dropdown {
display: none;
margin-right: 10px;
@media (min-width: $screen-sm-min) {
display: block;
}
}
.dropdown-menu-toggle {
width: 100px;
@media (min-width: $screen-md-min) {
width: 140px;
}
}
}
...@@ -159,7 +159,6 @@ ...@@ -159,7 +159,6 @@
.commit-row-description { .commit-row-description {
font-size: 14px; font-size: 14px;
border-left: 1px solid $white-normal;
padding: 10px 15px; padding: 10px 15px;
margin: 10px 0; margin: 10px 0;
background: $gray-light; background: $gray-light;
......
...@@ -193,7 +193,7 @@ ...@@ -193,7 +193,7 @@
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 10; z-index: 8;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
......
...@@ -148,3 +148,7 @@ ul.related-merge-requests > li { ...@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
} }
} }
.recaptcha {
margin-bottom: 30px;
}
...@@ -259,3 +259,8 @@ ...@@ -259,3 +259,8 @@
} }
} }
} }
.label-link {
display: inline-block;
vertical-align: text-top;
}
...@@ -80,6 +80,10 @@ ...@@ -80,6 +80,10 @@
.ci_widget { .ci_widget {
border-bottom: 1px solid $well-inner-border; border-bottom: 1px solid $well-inner-border;
color: $gl-text-color; color: $gl-text-color;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
svg { svg {
margin-right: 4px; margin-right: 4px;
...@@ -88,12 +92,20 @@ ...@@ -88,12 +92,20 @@
overflow: visible; overflow: visible;
} }
&> span {
padding-right: 4px;
}
&.ci-success_with_warnings { &.ci-success_with_warnings {
i { i {
color: $gl-warning; color: $gl-warning;
} }
} }
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
} }
.mr-widget-body, .mr-widget-body,
...@@ -102,6 +114,37 @@ ...@@ -102,6 +114,37 @@
padding: $gl-padding; padding: $gl-padding;
} }
.mr-widget-pipeline-graph {
flex-shrink: 0;
.dropdown-menu {
margin-top: 11px;
}
.ci-action-icon-wrapper {
line-height: 16px;
}
@media (max-width: $screen-xs-max) {
order: 1;
margin-top: $gl-padding-top;
border-radius: 3px;
background-color: $white-light;
border: 1px solid $gray-darker;
width: 100%;
text-align: center;
.dropdown-menu {
margin-left: -97.5px;
}
.arrow-up::before,
.arrow-up::after, {
margin-left: 97.5px;
}
}
}
.normal { .normal {
color: $gl-text-color; color: $gl-text-color;
} }
......
...@@ -183,52 +183,11 @@ ...@@ -183,52 +183,11 @@
} }
} }
.stage-cell {
font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
.stage-container {
display: inline-block;
position: relative;
height: 22px;
margin: 3px 6px 3px 0;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 10px;
border-bottom: 2px solid $border-color;
}
}
}
}
.duration, .duration,
.finished-at { .finished-at {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
margin: 4px 0; margin: 4px 0;
white-space: nowrap;
.fa { .fa {
font-size: 12px; font-size: 12px;
...@@ -311,6 +270,48 @@ ...@@ -311,6 +270,48 @@
} }
} }
.stage-cell {
font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
.stage-container {
display: inline-block;
position: relative;
height: 22px;
margin: 3px 6px 3px 0;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 10px;
border-bottom: 2px solid $border-color;
}
}
}
}
.admin-builds-table { .admin-builds-table {
.ci-table td:last-child { .ci-table td:last-child {
min-width: 120px; min-width: 120px;
...@@ -666,7 +667,7 @@ ...@@ -666,7 +667,7 @@
vertical-align: bottom; vertical-align: bottom;
display: inline-block; display: inline-block;
position: relative; position: relative;
font-weight: 200; font-weight: normal;
} }
// Dropdown button in mini pipeline graph // Dropdown button in mini pipeline graph
......
...@@ -147,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -147,6 +147,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_default_external, :user_default_external,
:user_oauth_applications, :user_oauth_applications,
:version_check_enabled, :version_check_enabled,
:terminal_max_session_time,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
......
module SpammableActions module SpammableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Recaptcha::Verify
included do included do
before_action :authorize_submit_spammable!, only: :mark_as_spam before_action :authorize_submit_spammable!, only: :mark_as_spam
end end
...@@ -15,6 +17,15 @@ module SpammableActions ...@@ -15,6 +17,15 @@ module SpammableActions
private private
def recaptcha_params
return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
{
recaptcha_verified: true,
spam_log_id: params[:spam_log_id]
}
end
def spammable def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}" raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end end
...@@ -22,4 +33,11 @@ module SpammableActions ...@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable! def authorize_submit_spammable!
access_denied! unless current_user.admin? access_denied! unless current_user.admin?
end end
def render_recaptcha?
return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
return false unless Gitlab::Recaptcha.enabled?
spammable.spam
end
end end
...@@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController
format.json do format.json do
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@pipelines) .represent(@pipelines)
end end
end end
......
...@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
def stop def stop
return render_404 unless @environment.stoppable? return render_404 unless @environment.available?
new_action = @environment.stop!(current_user) stop_action = @environment.stop_with_action!(current_user)
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
if stop_action
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
else
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
end
end end
def terminal def terminal
......
...@@ -98,15 +98,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -98,15 +98,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create def create
extra_params = { request: request, extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
extra_params.merge!(recaptcha_params)
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
if @issue.valid? html_response_create
redirect_to issue_path(@issue)
else
render :new
end
end end
format.js do format.js do
@link = @issue.attachment.url.to_js @link = @issue.attachment.url.to_js
...@@ -183,6 +181,20 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -183,6 +181,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected protected
def html_response_create
if @issue.valid?
redirect_to issue_path(@issue)
elsif render_recaptcha?
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
render :verify
else
render :new
end
end
def issue def issue
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
......
...@@ -221,19 +221,24 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -221,19 +221,24 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
format.json do format.json do
render json: { render json: PipelineSerializer
html: view_to_html_string('projects/merge_requests/show/_pipelines'), .new(project: @project, user: @current_user)
pipelines: PipelineSerializer .represent(@pipelines)
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@pipelines)
}
end end
end end
end end
def new def new
define_new_vars respond_to do |format|
format.html { define_new_vars }
format.json do
define_pipelines_vars
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent(@pipelines)
end
end
end end
def new_diffs def new_diffs
...@@ -479,7 +484,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -479,7 +484,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url = stop_url =
if environment.stoppable? && can?(current_user, :create_deployment, environment) if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment) stop_namespace_project_environment_path(project.namespace, project, environment)
end end
......
...@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
def show def show
@ref = params[:ref] || @project.default_branch || 'master' redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params)
@badges = [Gitlab::Badge::Build::Status,
Gitlab::Badge::Coverage::Report]
@badges.map! do |badge|
badge.new(@project, @ref).metadata
end
end end
def update def update
if @project.update_attributes(update_params) if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else else
render 'show' render 'show'
end end
......
...@@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
def index def index
@project_runners = project.runners.ordered redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
@assignable_runners = current_user.ci_authorized_runners.
assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end end
def edit def edit
...@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners def toggle_shared_runners
project.toggle!(:shared_runners_enabled) project.toggle!(:shared_runners_enabled)
redirect_to namespace_project_runners_path(project.namespace, project) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end end
protected protected
......
module Projects
module Settings
class CiCdController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
define_runners_variables
define_secret_variables
define_triggers_variables
define_badges_variables
end
private
def define_runners_variables
@project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners.
assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
def define_secret_variables
@variable = Ci::Variable.new
end
def define_triggers_variables
@triggers = @project.triggers
@trigger = Ci::Trigger.new
end
def define_badges_variables
@ref = params[:ref] || @project.default_branch || 'master'
@badges = [Gitlab::Badge::Build::Status,
Gitlab::Badge::Coverage::Report]
@badges.map! do |badge|
badge.new(@project, @ref).metadata
end
end
end
end
end
...@@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
def index def index
@triggers = project.triggers redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
@trigger = Ci::Trigger.new
end end
def create def create
...@@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger.save @trigger.save
if @trigger.valid? if @trigger.valid?
redirect_to namespace_project_triggers_path(@project.namespace, @project) redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
else else
@triggers = project.triggers.select(&:persisted?) @triggers = project.triggers.select(&:persisted?)
render :index render action: "show"
end end
end end
def destroy def destroy
trigger.destroy trigger.destroy
flash[:alert] = "Trigger removed"
redirect_to namespace_project_triggers_path(@project.namespace, @project) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end end
private private
......
...@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
def index def index
@variable = Ci::Variable.new redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end end
def show def show
...@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController
@variable = Ci::Variable.new(project_params) @variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable if @variable.valid? && @project.variables << @variable
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' flash[:notice] = 'Variables were successfully updated.'
redirect_to namespace_project_settings_ci_cd_path(project.namespace, project)
else else
render action: "index" render "show"
end end
end end
...@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController ...@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController
@key = @project.variables.find(params[:id]) @key = @project.variables.find(params[:id])
@key.destroy @key.destroy
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.' redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.'
end end
private private
......
...@@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super super
else else
flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error flash.delete :recaptcha_error
render action: 'new' render action: 'new'
end end
...@@ -30,7 +30,7 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -30,7 +30,7 @@ class RegistrationsController < Devise::RegistrationsController
format.html do format.html do
session.try(:destroy) session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed." redirect_to new_user_session_path, notice: "Account successfully removed."
end end
end end
end end
......
...@@ -211,8 +211,12 @@ module GitlabRoutingHelper ...@@ -211,8 +211,12 @@ module GitlabRoutingHelper
def project_settings_integrations_path(project, *args) def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args) namespace_project_settings_integrations_path(project.namespace, project, *args)
end end
def project_settings_members_path(project, *args) def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args) namespace_project_settings_members_path(project.namespace, project, *args)
end end
def project_settings_ci_cd_path(project, *args)
namespace_project_settings_ci_cd_path(project.namespace, project, *args)
end
end end
...@@ -20,8 +20,8 @@ module MergeRequestsHelper ...@@ -20,8 +20,8 @@ module MergeRequestsHelper
end end
def mr_widget_refresh_url(mr) def mr_widget_refresh_url(mr)
if mr && mr.source_project if mr && mr.target_project
merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else else
'' ''
end end
......
...@@ -124,6 +124,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -124,6 +124,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
validates :terminal_max_session_time,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil? unless value.nil?
value.each do |level| value.each do |level|
...@@ -217,7 +221,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -217,7 +221,8 @@ class ApplicationSetting < ActiveRecord::Base
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false user_default_external: false,
terminal_max_session_time: 0
} }
end end
......
...@@ -11,6 +11,7 @@ module Spammable ...@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam attr_accessor :spam
attr_accessor :spam_log
after_validation :check_for_spam, on: :create after_validation :check_for_spam, on: :create
...@@ -34,9 +35,14 @@ module Spammable ...@@ -34,9 +35,14 @@ module Spammable
end end
def check_for_spam def check_for_spam
if spam? error_msg = if Gitlab::Recaptcha.enabled?
self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") "Your #{spammable_entity_type} has been recognized as spam. "\
end "You can still submit it by solving Captcha."
else
"Your #{spammable_entity_type} has been recognized as spam and has been discarded."
end
self.errors.add(:base, error_msg) if spam?
end end
def spammable_entity_type def spammable_entity_type
......
...@@ -18,7 +18,7 @@ module TimeTrackable ...@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent validate :check_negative_time_spent
has_many :timelogs, as: :trackable, dependent: :destroy has_many :timelogs, dependent: :destroy
end end
def spend_time(options) def spend_time(options)
......
...@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base ...@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop) @stop_action ||= manual_actions.find_by(name: on_stop)
end end
def stoppable? def stop_action?
stop_action.present? stop_action.present?
end end
......
...@@ -118,15 +118,15 @@ class Environment < ActiveRecord::Base ...@@ -118,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '') external_url.gsub(/\A.*?:\/\//, '')
end end
def stoppable? def stop_action?
available? && stop_action.present? available? && stop_action.present?
end end
def stop!(current_user) def stop_with_action!(current_user)
return unless stoppable? return unless available?
stop stop!
stop_action.play(current_user) stop_action.play(current_user) if stop_action
end end
def actions_for(environment) def actions_for(environment)
......
...@@ -241,7 +241,12 @@ class Group < Namespace ...@@ -241,7 +241,12 @@ class Group < Namespace
end end
def refresh_members_authorized_projects def refresh_members_authorized_projects
UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
execute
end
def user_ids_for_project_authorizations
users_with_parents.pluck(:id)
end end
def members_with_parents def members_with_parents
......
...@@ -218,6 +218,10 @@ class Namespace < ActiveRecord::Base ...@@ -218,6 +218,10 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end end
def user_ids_for_project_authorizations
[owner_id]
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -55,6 +55,7 @@ class Project < ActiveRecord::Base ...@@ -55,6 +55,7 @@ class Project < ActiveRecord::Base
after_destroy :remove_pages after_destroy :remove_pages
# update visibility_level of forks
after_update :update_forks_visibility_level after_update :update_forks_visibility_level
after_update :remove_mirror_repository_reference, after_update :remove_mirror_repository_reference,
if: ->(project) { project.mirror? && project.import_url_updated? } if: ->(project) { project.mirror? && project.import_url_updated? }
......
...@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service ...@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: '' } { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
] ]
end end
......
class KubernetesService < DeploymentService class KubernetesService < DeploymentService
include Gitlab::CurrentSettings
include Gitlab::Kubernetes include Gitlab::Kubernetes
include ReactiveCaching include ReactiveCaching
...@@ -110,7 +111,7 @@ class KubernetesService < DeploymentService ...@@ -110,7 +111,7 @@ class KubernetesService < DeploymentService
pods = data.fetch(:pods, nil) pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug). filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
map { |terminal| add_terminal_auth(terminal, token, ca_pem) } each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end end
end end
...@@ -170,4 +171,12 @@ class KubernetesService < DeploymentService ...@@ -170,4 +171,12 @@ class KubernetesService < DeploymentService
url.to_s url.to_s
end end
def terminal_auth
{
token: token,
ca_pem: ca_pem,
max_session_time: current_application_settings.terminal_max_session_time
}
end
end end
...@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService ...@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end end
def title def title
'Mattermost Command' 'Mattermost slash commands'
end end
def description def description
"Perform common operations on GitLab in Mattermost" "Perform common operations in Mattermost"
end end
def self.to_param def self.to_param
......
...@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService ...@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper include TriggersHelper
def title def title
'Slack Command' 'Slack slash commands'
end end
def description def description
"Perform common operations on GitLab in Slack" "Perform common operations in Slack"
end end
def self.to_param def self.to_param
......
class Timelog < ActiveRecord::Base class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true validates :time_spent, :user, presence: true
validate :issuable_id_is_present
belongs_to :trackable, polymorphic: true belongs_to :issue
belongs_to :merge_request
belongs_to :user belongs_to :user
def issuable
issue || merge_request
end
private
def issuable_id_is_present
if issue_id && merge_request_id
errors.add(:base, 'Only Issue ID or Merge Request ID is required')
elsif issuable.nil?
errors.add(:base, 'Issue or Merge Request ID is required')
end
end
end end
...@@ -103,6 +103,9 @@ class User < ActiveRecord::Base ...@@ -103,6 +103,9 @@ class User < ActiveRecord::Base
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
# #
# Validations # Validations
# #
......
...@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity ...@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url expose :external_url
expose :environment_type expose :environment_type
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stoppable? expose :stop_action?
expose :environment_path do |environment| expose :environment_path do |environment|
namespace_project_environment_path( namespace_project_environment_path(
......
...@@ -8,10 +8,9 @@ module Ci ...@@ -8,10 +8,9 @@ module Ci
return unless has_ref? return unless has_ref?
environments.each do |environment| environments.each do |environment|
next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project) next unless can?(current_user, :create_deployment, project)
environment.stop!(current_user) environment.stop_with_action!(current_user)
end end
end end
......
...@@ -3,6 +3,8 @@ module Issues ...@@ -3,6 +3,8 @@ module Issues
def execute def execute
@request = params.delete(:request) @request = params.delete(:request)
@api = params.delete(:api) @api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute @issue = BuildService.new(project, current_user, issue_attributes).execute
...@@ -11,7 +13,13 @@ module Issues ...@@ -11,7 +13,13 @@ module Issues
end end
def before_create(issuable) def before_create(issuable)
issuable.spam = spam_service.check(@api) if @recaptcha_verified
spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
spam_log.update!(recaptcha_verified: true) if spam_log
else
issuable.spam = spam_service.check(@api)
issuable.spam_log = spam_service.spam_log
end
end end
def after_create(issuable) def after_create(issuable)
...@@ -35,7 +43,7 @@ module Issues ...@@ -35,7 +43,7 @@ module Issues
private private
def spam_service def spam_service
SpamService.new(@issue, @request) @spam_service ||= SpamService.new(@issue, @request)
end end
def user_agent_detail_service def user_agent_detail_service
......
...@@ -26,7 +26,7 @@ module Projects ...@@ -26,7 +26,7 @@ module Projects
end end
def project_tree_saver def project_tree_saver
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared) Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
end end
def uploads_saver def uploads_saver
......
...@@ -25,9 +25,10 @@ module Projects ...@@ -25,9 +25,10 @@ module Projects
end end
def transfer(project, new_namespace) def transfer(project, new_namespace)
old_namespace = project.namespace
Project.transaction do Project.transaction do
old_path = project.path_with_namespace old_path = project.path_with_namespace
old_namespace = project.namespace
old_group = project.group old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path) new_path = File.join(new_namespace.try(:path) || '', project.path)
...@@ -70,8 +71,11 @@ module Projects ...@@ -70,8 +71,11 @@ module Projects
project.old_path_with_namespace = old_path project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end end
refresh_permissions(old_namespace, new_namespace)
true
end end
def allowed_transfer?(current_user, project, namespace) def allowed_transfer?(current_user, project, namespace)
...@@ -80,5 +84,14 @@ module Projects ...@@ -80,5 +84,14 @@ module Projects
namespace.id != project.namespace_id && namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, namespace)
end end
def refresh_permissions(old_namespace, new_namespace)
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute
end
end end
end end
class SpamService class SpamService
attr_accessor :spammable, :request, :options attr_accessor :spammable, :request, :options
attr_reader :spam_log
def initialize(spammable, request = nil) def initialize(spammable, request = nil)
@spammable = spammable @spammable = spammable
...@@ -63,7 +64,7 @@ class SpamService ...@@ -63,7 +64,7 @@ class SpamService
end end
def create_spam_log(api) def create_spam_log(api)
SpamLog.create( @spam_log = SpamLog.create!(
{ {
user_id: spammable_owner_id, user_id: spammable_owner_id,
title: spammable.spam_title, title: spammable.spam_title,
......
...@@ -118,16 +118,18 @@ module SystemNoteService ...@@ -118,16 +118,18 @@ module SystemNoteService
# #
# Example Note text: # Example Note text:
# #
# "Changed estimate of this issue to 3d 5h" # "removed time estimate"
#
# "changed time estimate to 3d 5h"
# #
# Returns the created Note object # Returns the created Note object
def change_time_estimate(noteable, project, author) def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0 body = if noteable.time_estimate == 0
"Removed time estimate on this #{noteable.human_class_name}" "removed time estimate"
else else
"Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" "changed time estimate to #{parsed_time}"
end end
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
...@@ -142,7 +144,9 @@ module SystemNoteService ...@@ -142,7 +144,9 @@ module SystemNoteService
# #
# Example Note text: # Example Note text:
# #
# "Added 2h 30m of time spent on this issue" # "removed time spent"
#
# "added 2h 30m of time spent"
# #
# Returns the created Note object # Returns the created Note object
...@@ -150,11 +154,11 @@ module SystemNoteService ...@@ -150,11 +154,11 @@ module SystemNoteService
time_spent = noteable.time_spent time_spent = noteable.time_spent
if time_spent == :reset if time_spent == :reset
body = "Removed time spent on this #{noteable.human_class_name}" body = "removed time spent"
else else
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'Added' : 'Subtracted' action = time_spent > 0 ? 'added' : 'subtracted'
body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" body = "#{action} #{parsed_time} of time spent"
end end
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
...@@ -221,7 +225,7 @@ module SystemNoteService ...@@ -221,7 +225,7 @@ module SystemNoteService
end end
def discussion_continued_in_issue(discussion, project, author, issue) def discussion_continued_in_issue(discussion, project, author, issue)
body = "Added #{issue.to_reference} to continue this discussion" body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
note_attributes[:type] = note_attributes.delete(:note_type) note_attributes[:type] = note_attributes.delete(:note_type)
...@@ -260,7 +264,7 @@ module SystemNoteService ...@@ -260,7 +264,7 @@ module SystemNoteService
# #
# Example Note text: # Example Note text:
# #
# "made the issue confidential" # "made the issue confidential"
# #
# Returns the created Note object # Returns the created Note object
def change_issue_confidentiality(issue, project, author) def change_issue_confidentiality(issue, project, author)
...@@ -381,6 +385,7 @@ module SystemNoteService ...@@ -381,6 +385,7 @@ module SystemNoteService
# Returns Boolean # Returns Boolean
def cross_reference_disallowed?(noteable, mentioner) def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
return false unless mentioner.is_a?(MergeRequest) return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit) return false unless noteable.is_a?(Commit)
......
...@@ -573,5 +573,15 @@ ...@@ -573,5 +573,15 @@
.help-block .help-block
Number of Git pushes after which 'git gc' is run. Number of Git pushes after which 'git gc' is run.
%fieldset
%legend Web terminal
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :terminal_max_session_time, class: 'form-control'
.help-block
Maximum time for web terminal websocket connection (in seconds).
Set to 0 for unlimited time.
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= icon("search", class: "search-icon") = icon("search", class: "search-icon")
.dropdown .dropdown
- toggle_text = 'Search for Namespace' - toggle_text = 'Namespace'
- if params[:namespace_id].present? - if params[:namespace_id].present?
- namespace = Namespace.find(params[:namespace_id]) - namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.path}" - toggle_text = "#{namespace.kind}: #{namespace.path}"
...@@ -37,8 +37,10 @@ ...@@ -37,8 +37,10 @@
= dropdown_filter("Search for Namespace") = dropdown_filter("Search for Namespace")
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
= render 'shared/projects/dropdown'
= button_tag "Search", class: "btn btn-primary btn-search" = link_to new_project_path, class: 'btn btn-new' do
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links %ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
...@@ -56,11 +58,6 @@ ...@@ -56,11 +58,6 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public Public
.nav-controls
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'btn btn-new' do
New Project
.projects-list-holder .projects-list-holder
- if @projects.any? - if @projects.any?
%ul.projects-list.content-list %ul.projects-list.content-list
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
= spam_log.source_ip = spam_log.source_ip
%td %td
= spam_log.via_api? ? 'Y' : 'N' = spam_log.via_api? ? 'Y' : 'N'
%td
= spam_log.recaptcha_verified ? 'Y' : 'N'
%td %td
= spam_log.noteable_type = spam_log.noteable_type
%td %td
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
%th User %th User
%th Source IP %th Source IP
%th API? %th API?
%th Recaptcha verified?
%th Type %th Type
%th Title %th Title
%th Description %th Description
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters %p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div %div
- if current_application_settings.recaptcha_enabled - if Gitlab::Recaptcha.enabled?
= recaptcha_tags = recaptcha_tags
%div %div
= f.submit "Register", class: "btn-register btn" = f.submit "Register", class: "btn-register btn"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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