Commit c2c34698 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee-2017-06-23' into 'master'

CE upstream: Friday

Closes gitaly#333, gitlab-ce#34010, #2696, and gitlab-ce#33868

See merge request !2230
parents 68918387 f0b91cac
...@@ -447,6 +447,7 @@ karma: ...@@ -447,6 +447,7 @@ karma:
- coverage-javascript/ - coverage-javascript/
codeclimate: codeclimate:
<<: *except-docs
before_script: [] before_script: []
image: docker:latest image: docker:latest
stage: test stage: test
......
Please read this! Please read this!
Before opening a new issue, make sure to search for keywords in the issues Before opening a new issue, make sure to search for keywords in the issues
filtered by the "regression" or "bug" label: filtered by the "regression" or "bug" label.
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate. and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate.
......
...@@ -3,8 +3,14 @@ Please read this! ...@@ -3,8 +3,14 @@ Please read this!
Before opening a new issue, make sure to search for keywords in the issues Before opening a new issue, make sure to search for keywords in the issues
filtered by the "feature proposal" label: filtered by the "feature proposal" label:
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
and verify the issue you're about to submit isn't a duplicate. and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate.
...@@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate. ...@@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate.
### Documentation blurb ### Documentation blurb
(Write the start of the documentation of this feature here, include: #### Overview
What is it?
Why should someone use this feature?
What is the underlying (business) problem?
How do you use this feature?
#### Use cases
Who is this for? Provide one or more use cases.
### Feature checklist
1. Why should someone use it; what's the underlying problem. Make sure these are completed before closing the issue,
2. What is the solution. with a link to the relevant commit.
3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.) - [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
- [ ] Documentation
- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
/label ~"feature proposal" /label ~"feature proposal"
\ No newline at end of file
...@@ -2,7 +2,6 @@ source 'https://rubygems.org' ...@@ -2,7 +2,6 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8' gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with # Responders respond_to and respond_with
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
...@@ -132,6 +131,7 @@ gem 'asciidoctor', '~> 1.5.2' ...@@ -132,6 +131,7 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7' gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8' gem 'truncato', '~> 0.7.8'
gem 'bootstrap_form', '~> 2.7.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -270,6 +270,7 @@ gem 'premailer-rails', '~> 1.9.0' ...@@ -270,6 +270,7 @@ gem 'premailer-rails', '~> 1.9.0'
# I18n # I18n
gem 'ruby_parser', '~> 3.8', require: false gem 'ruby_parser', '~> 3.8', require: false
gem 'rails-i18n', '~> 4.0.9'
gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
...@@ -393,7 +394,7 @@ gem 'vmstat', '~> 2.3.0' ...@@ -393,7 +394,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.8.0' gem 'gitaly', '~> 0.9.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -91,11 +91,10 @@ GEM ...@@ -91,11 +91,10 @@ GEM
bindata (2.3.5) bindata (2.3.5)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (3.6.1) brakeman (3.6.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.3) builder (3.2.3)
...@@ -301,7 +300,7 @@ GEM ...@@ -301,7 +300,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.8.0) gitaly (0.9.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -492,7 +491,6 @@ GEM ...@@ -492,7 +491,6 @@ GEM
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.6) mmap2 (2.2.6)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
...@@ -674,6 +672,9 @@ GEM ...@@ -674,6 +672,9 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
rails-i18n (4.0.9)
i18n (~> 0.7)
railties (~> 4.0)
railties (4.2.8) railties (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
activesupport (= 4.2.8) activesupport (= 4.2.8)
...@@ -954,8 +955,8 @@ DEPENDENCIES ...@@ -954,8 +955,8 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0) brakeman (~> 3.6.0)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.5.0) bullet (~> 5.5.0)
...@@ -1007,7 +1008,7 @@ DEPENDENCIES ...@@ -1007,7 +1008,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.8.0) gitaly (~> 0.9.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
...@@ -1090,6 +1091,7 @@ DEPENDENCIES ...@@ -1090,6 +1091,7 @@ DEPENDENCIES
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 4.2.8) rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 2.2)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
......
This diff is collapsed.
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
if (this.title.trim() === '') return; if (this.title.trim() === '') return Promise.resolve();
this.error = false; this.error = false;
...@@ -33,7 +33,10 @@ export default { ...@@ -33,7 +33,10 @@ export default {
issue.milestone_id = Store.state.currentBoard.milestone_id; issue.milestone_id = Store.state.currentBoard.milestone_id;
} }
this.list.newIssue(issue) eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
...@@ -51,9 +54,6 @@ export default { ...@@ -51,9 +54,6 @@ export default {
// Show error message // Show error message
this.error = true; this.error = true;
}); });
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
......
...@@ -113,8 +113,7 @@ class List { ...@@ -113,8 +113,7 @@ class List {
const data = resp.json(); const data = resp.json();
issue.id = data.iid; issue.id = data.iid;
issue.milestone = data.milestone; issue.milestone = data.milestone;
})
.then(() => {
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id; const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
......
/* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import commitPipelinesTable from './pipelines_table.vue';
import CommitPipelinesTable from './pipelines_table';
Vue.use(VueResource);
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Used in:
* * - Commit details View > Pipelines Tab > Pipelines Table.
* Renders Pipelines table in pipelines tab in the commits show view. * - Merge Request details View > Pipelines Tab > Pipelines Table.
* - New Merge Request View > Pipelines Tab > Pipelines Table.
*/ */
// export for use in merge_request_tabs.js (TODO: remove this hack) const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load
// vue.js in merge_request_tabs.js)
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable; window.gl.CommitPipelinesTable = CommitPipelinesTable;
$(() => { document.addEventListener('DOMContentLoaded', () => {
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); const table = new CommitPipelinesTable({
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
} }
}); });
<script>
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default {
props: {
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
mixins: [
pipelinesMixin,
],
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
},
computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
},
methods: {
successCallback(resp) {
const response = resp.json();
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.setCommonData(pipelines);
},
},
};
</script>
<template>
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
/>
<error-state
v-if="shouldRenderErrorState"
/>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
</template>
...@@ -404,6 +404,14 @@ export default { ...@@ -404,6 +404,14 @@ export default {
return ''; return '';
}, },
displayEnvironmentActions() {
return this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.hasStopAction ||
this.canRetry;
},
/** /**
* Constructs folder URL based on the current location and the folder id. * Constructs folder URL based on the current location and the folder id.
* *
...@@ -551,9 +559,12 @@ export default { ...@@ -551,9 +559,12 @@ export default {
</span> </span>
</div> </div>
<div class="table-section section-30 table-button-footer" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder && displayEnvironmentActions"
class="table-section section-30 table-button-footer"
role="gridcell">
<div
class="btn-group table-action-buttons" class="btn-group table-action-buttons"
role="group"> role="group">
......
...@@ -44,6 +44,10 @@ class FilteredSearchManager { ...@@ -44,6 +44,10 @@ class FilteredSearchManager {
return []; return [];
}) })
.then((searches) => { .then((searches) => {
if (!searches) {
return;
}
// Put any searches that may have come in before // Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones // we fetched the saved searches ahead of the already saved ones
const resultantSearches = this.recentSearchesStore.setRecentSearches( const resultantSearches = this.recentSearchesStore.setRecentSearches(
...@@ -491,6 +495,7 @@ class FilteredSearchManager { ...@@ -491,6 +495,7 @@ class FilteredSearchManager {
} }
searchState(e) { searchState(e) {
e.preventDefault();
const target = e.currentTarget; const target = e.currentTarget;
// remove focus outline after click // remove focus outline after click
target.blur(); target.blur();
......
...@@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar {
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar'); this.$page = $('.page-with-sidebar');
this.$sidebar = $('.right-sidebar'); this.$sidebar = $('.right-sidebar');
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
this.$bulkEditSubmitBtn = $('.update-selected-issues'); this.$bulkEditSubmitBtn = $('.update-selected-issues');
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle'); this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
...@@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar {
toggleSidebarDisplay(show) { toggleSidebarDisplay(show) {
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show);
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
} }
......
...@@ -51,6 +51,11 @@ export default { ...@@ -51,6 +51,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: { updatedAt: {
type: String, type: String,
required: false, required: false,
...@@ -105,6 +110,7 @@ export default { ...@@ -105,6 +110,7 @@ export default {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
}); });
return { return {
...@@ -198,13 +204,7 @@ export default { ...@@ -198,13 +204,7 @@ export default {
method: 'getData', method: 'getData',
successCallback: (res) => { successCallback: (res) => {
const data = res.json(); const data = res.json();
const shouldUpdate = this.store.stateShouldUpdate(data);
this.store.updateState(data); this.store.updateState(data);
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
this.store.formState.lockedWarningVisible = true;
}
}, },
errorCallback(err) { errorCallback(err) {
throw new Error(err); throw new Error(err);
......
...@@ -37,18 +37,7 @@ ...@@ -37,18 +37,7 @@
}); });
}, },
taskStatus() { taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); this.updateTaskStatusText();
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
}, },
}, },
methods: { methods: {
...@@ -64,9 +53,24 @@ ...@@ -64,9 +53,24 @@
}); });
} }
}, },
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
}, },
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
this.updateTaskStatusText();
}, },
}; };
</script> </script>
......
...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
updatedByName: this.updatedByName, updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath, updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
}, },
}); });
}, },
......
export default class Store { export default class Store {
constructor({ constructor(initialState) {
titleHtml, this.state = initialState;
titleText,
descriptionHtml,
descriptionText,
updatedAt,
updatedByName,
updatedByPath,
}) {
this.state = {
titleHtml,
titleText,
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt,
updatedByName,
updatedByPath,
};
this.formState = { this.formState = {
title: '', title: '',
confidential: false, confidential: false,
...@@ -29,6 +12,10 @@ export default class Store { ...@@ -29,6 +12,10 @@ export default class Store {
} }
updateState(data) { updateState(data) {
if (this.stateShouldUpdate(data)) {
this.formState.lockedWarningVisible = true;
}
this.state.titleHtml = data.title; this.state.titleHtml = data.title;
this.state.titleText = data.title_text; this.state.titleText = data.title_text;
this.state.descriptionHtml = data.description; this.state.descriptionHtml = data.description;
...@@ -40,10 +27,8 @@ export default class Store { ...@@ -40,10 +27,8 @@ export default class Store {
} }
stateShouldUpdate(data) { stateShouldUpdate(data) {
return { return this.state.titleText !== data.title_text ||
title: this.state.titleText !== data.title_text, this.state.descriptionText !== data.description_text;
description: this.state.descriptionText !== data.description_text,
};
} }
setFormState(state) { setFormState(state) {
......
...@@ -86,18 +86,25 @@ ...@@ -86,18 +86,25 @@
// This is required to handle non-unicode characters in hash // This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash); hash = decodeURIComponent(hash);
var fixedTabs = document.querySelector('.js-tabs-affix');
var fixedNav = document.querySelector('.navbar-gitlab');
var adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
// scroll to user-generated markdown anchor if we cannot find a match // scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) { if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash); var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) { if (target && target.scrollIntoView) {
target.scrollIntoView(true); target.scrollIntoView(true);
window.scrollBy(0, adjustment);
} }
} else { } else {
// only adjust for fixedTabs when not targeting user-generated content // only adjust for fixedTabs when not targeting user-generated content
var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) { if (fixedTabs) {
window.scrollBy(0, -fixedTabs.offsetHeight); adjustment -= fixedTabs.offsetHeight;
} }
window.scrollBy(0, adjustment);
} }
}; };
......
This diff is collapsed.
This diff is collapsed.
...@@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
scrollToElement(container) { scrollToElement(container) {
if (location.hash) { if (location.hash) {
const offset = -$('.js-tabs-affix').outerHeight(); const offset = 0 - (
$('.navbar-gitlab').outerHeight() +
$('.js-tabs-affix').outerHeight()
);
const $el = $(`${container} ${location.hash}:not(.match)`); const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) { if ($el.length) {
$.scrollTo($el[0], { offset }); $.scrollTo($el[0], { offset });
...@@ -233,11 +236,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -233,11 +236,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
} }
mountPipelinesView() { mountPipelinesView() {
this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
const CommitPipelinesTable = gl.CommitPipelinesTable;
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
// $mount(el) replaces the el with the new rendered component. We need it in order to mount // $mount(el) replaces the el with the new rendered component. We need it in order to mount
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view') pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
.appendChild(this.commitPipelinesTable.$el);
} }
loadDiff(source) { loadDiff(source) {
...@@ -294,6 +304,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -294,6 +304,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
forceShow: true, forceShow: true,
}); });
anchor[0].scrollIntoView(); anchor[0].scrollIntoView();
window.gl.utils.handleLocationHash();
// We have multiple elements on the page with `#note_xxx` // We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first // (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target'); anchor.addClass('target');
......
This diff is collapsed.
<script> <script>
/* eslint-disable no-new, no-alert */ /* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default { export default {
props: { props: {
...@@ -11,53 +11,42 @@ export default { ...@@ -11,53 +11,42 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
title: { title: {
type: String, type: String,
required: true, required: true,
}, },
icon: { icon: {
type: String, type: String,
required: true, required: true,
}, },
cssClass: { cssClass: {
type: String, type: String,
required: true, required: true,
}, },
confirmActionMessage: { confirmActionMessage: {
type: String, type: String,
required: false, required: false,
}, },
}, },
components: { components: {
loadingIcon, loadingIcon,
}, },
mixins: [
tooltipMixin,
],
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
iconClass() { iconClass() {
return `fa fa-${this.icon}`; return `fa fa-${this.icon}`;
}, },
buttonClass() { buttonClass() {
return `btn has-tooltip ${this.cssClass}`; return `btn ${this.cssClass}`;
}, },
}, },
methods: { methods: {
onClick() { onClick() {
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
...@@ -66,21 +55,11 @@ export default { ...@@ -66,21 +55,11 @@ export default {
this.makeRequest(); this.makeRequest();
} }
}, },
makeRequest() { makeRequest() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
}, },
}; };
...@@ -95,10 +74,12 @@ export default { ...@@ -95,10 +74,12 @@ export default {
:aria-label="title" :aria-label="title"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
ref="tooltip"
:disabled="isLoading"> :disabled="isLoading">
<i <i
:class="iconClass" :class="iconClass"
aria-hidden="true" /> aria-hidden="true">
</i>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
</button> </button>
</template> </template>
<script> <script>
import Visibility from 'visibilityjs';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import eventHub from '../event_hub'; import pipelinesMixin from '../mixins/pipelines';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import emptyState from './empty_state.vue';
import errorState from './error_state.vue';
import navigationTabs from './navigation_tabs.vue'; import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import navigationControls from './nav_controls.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Poll from '../../lib/utils/poll';
export default { export default {
props: { props: {
...@@ -20,13 +14,12 @@ ...@@ -20,13 +14,12 @@
}, },
components: { components: {
tablePagination, tablePagination,
pipelinesTableComponent,
emptyState,
errorState,
navigationTabs, navigationTabs,
navigationControls, navigationControls,
loadingIcon,
}, },
mixins: [
pipelinesMixin,
],
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
...@@ -47,11 +40,6 @@ ...@@ -47,11 +40,6 @@
state: this.store.state, state: this.store.state,
apiScope: 'all', apiScope: 'all',
pagenum: 1, pagenum: 1,
isLoading: false,
hasError: false,
isMakingRequest: false,
updateGraphDropdown: false,
hasMadeRequest: false,
}; };
}, },
computed: { computed: {
...@@ -62,9 +50,6 @@ ...@@ -62,9 +50,6 @@
const scope = gl.utils.getParameterByName('scope'); const scope = gl.utils.getParameterByName('scope');
return scope === null ? 'all' : scope; return scope === null ? 'all' : scope;
}, },
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * The empty state should only be rendered when the request is made to fetch all pipelines
...@@ -106,7 +91,6 @@ ...@@ -106,7 +91,6 @@
this.state.pipelines.length && this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage; this.state.pageInfo.total > this.state.pageInfo.perPage;
}, },
hasCiEnabled() { hasCiEnabled() {
return this.hasCi !== undefined; return this.hasCi !== undefined;
}, },
...@@ -129,37 +113,7 @@ ...@@ -129,37 +113,7 @@
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
} else {
// If tab is not visible we need to make the first request so we don't show the empty
// state without knowing if there are any pipelines
this.fetchPipelines();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('refreshPipelines');
}, },
methods: { methods: {
/** /**
...@@ -174,15 +128,6 @@ ...@@ -174,15 +128,6 @@
return param; return param;
}, },
fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
},
successCallback(resp) { successCallback(resp) {
const response = { const response = {
headers: resp.headers, headers: resp.headers,
...@@ -190,33 +135,14 @@ ...@@ -190,33 +135,14 @@
}; };
this.store.storeCount(response.body.count); this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
this.setCommonData(response.body.pipelines);
this.isLoading = false;
this.updateGraphDropdown = true;
this.hasMadeRequest = true;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
}; };
</script> </script>
<template> <template>
<div :class="cssClass"> <div :class="cssClass">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState"> v-if="!isLoading && !shouldRenderEmptyState">
...@@ -274,7 +200,6 @@ ...@@ -274,7 +200,6 @@
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
/> />
</div> </div>
......
...@@ -11,10 +11,6 @@ ...@@ -11,10 +11,6 @@
type: Array, type: Array,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
}, },
components: { components: {
loadingIcon, loadingIcon,
...@@ -31,17 +27,9 @@ ...@@ -31,17 +27,9 @@
$(this.$refs.tooltip).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
this.service.postAction(endpoint) eventHub.$emit('postAction', endpoint);
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occured while making the request.');
});
}, },
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
......
...@@ -12,10 +12,6 @@ ...@@ -12,10 +12,6 @@
type: Array, type: Array,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -57,7 +53,6 @@ ...@@ -57,7 +53,6 @@
v-for="model in pipelines" v-for="model in pipelines"
:key="model.id" :key="model.id"
:pipeline="model" :pipeline="model"
:service="service"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
/> />
</div> </div>
......
<script> <script>
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import asyncButtonComponent from '../../pipelines/components/async_button.vue'; import asyncButtonComponent from './async_button.vue';
import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; import pipelinesActionsComponent from './pipelines_actions.vue';
import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
import ciBadge from './ci_badge_link.vue'; import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
import pipelineStage from '../../pipelines/components/stage.vue'; import pipelineStage from './stage.vue';
import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; import pipelineUrl from './pipeline_url.vue';
import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; import pipelinesTimeago from './time_ago.vue';
import commitComponent from './commit.vue'; import commitComponent from '../../vue_shared/components/commit.vue';
/** /**
* Pipeline table row. * Pipeline table row.
...@@ -20,10 +20,6 @@ export default { ...@@ -20,10 +20,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
service: {
type: Object,
required: true,
},
updateGraphDropdown: { updateGraphDropdown: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -271,7 +267,6 @@ export default { ...@@ -271,7 +267,6 @@ export default {
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions" :actions="pipeline.details.manual_actions"
:service="service"
/> />
<pipelines-artifacts-component <pipelines-artifacts-component
...@@ -282,7 +277,6 @@ export default { ...@@ -282,7 +277,6 @@ export default {
<async-button-component <async-button-component
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
:service="service"
:endpoint="pipeline.retry_path" :endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
...@@ -291,7 +285,6 @@ export default { ...@@ -291,7 +285,6 @@ export default {
<async-button-component <async-button-component
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:service="service"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
......
import Vue from 'vue'; /* global Flash */
import '~/flash';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue';
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import eventHub from '../../pipelines/event_hub';
import emptyState from '../../pipelines/components/empty_state.vue';
import errorState from '../../pipelines/components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
/** export default {
*
* 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.
*
*/
export default Vue.component('pipelines-table', {
components: { components: {
pipelinesTableComponent, pipelinesTableComponent,
errorState, errorState,
emptyState, emptyState,
loadingIcon, loadingIcon,
}, },
computed: {
/** shouldRenderErrorState() {
* Accesses the DOM to provide the needed data. return this.hasError && !this.isLoading;
* Returns the necessary props to render `pipelines-table-component` component. },
* },
* @return {Object}
*/
data() { data() {
const store = new PipelineStore();
return { return {
endpoint: null,
helpPagePath: null,
store,
state: store.state,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
...@@ -50,49 +29,11 @@ export default Vue.component('pipelines-table', { ...@@ -50,49 +29,11 @@ export default Vue.component('pipelines-table', {
hasMadeRequest: false, hasMadeRequest: false,
}; };
}, },
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
},
/**
* When the component is about to be mounted, tell the service to fetch the data
*
* 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.
*
*/
beforeMount() { beforeMount() {
const element = document.querySelector('#commit-pipeline-table-view');
this.endpoint = element.dataset.endpoint;
this.helpPagePath = element.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint);
this.poll = new Poll({ this.poll = new Poll({
resource: this.service, resource: this.service,
method: 'getPipelines', method: 'getPipelines',
data: this.requestData ? this.requestData : undefined,
successCallback: this.successCallback, successCallback: this.successCallback,
errorCallback: this.errorCallback, errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest, notificationCallback: this.setIsMakingRequest,
...@@ -116,43 +57,36 @@ export default Vue.component('pipelines-table', { ...@@ -116,43 +57,36 @@ export default Vue.component('pipelines-table', {
}); });
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
eventHub.$on('postAction', this.postAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
eventHub.$on('postAction', this.postAction);
}, },
destroyed() { destroyed() {
this.poll.stop(); this.poll.stop();
}, },
methods: { methods: {
fetchPipelines() { fetchPipelines() {
if (!this.isMakingRequest) {
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines() this.service.getPipelines(this.requestData)
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback());
}
}, },
setCommonData(pipelines) {
successCallback(resp) {
const response = resp.json();
this.hasMadeRequest = true;
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true; this.updateGraphDropdown = true;
this.hasMadeRequest = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
...@@ -160,32 +94,10 @@ export default Vue.component('pipelines-table', { ...@@ -160,32 +94,10 @@ export default Vue.component('pipelines-table', {
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
} }
}, },
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occured while making the request.'));
}, },
},
template: ` };
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" />
<error-state v-if="shouldRenderErrorState" />
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
`,
});
export default {
EMPTY: 'empty',
LOADING: 'loading',
LIST: 'list',
};
import PrometheusMetrics from './prometheus_metrics';
$(() => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
});
import PANEL_STATE from './constants';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
this.backOffRequestCounter = 0;
this.$wrapper = $(wrapperSelector);
this.$monitoredMetricsPanel = this.$wrapper.find('.js-panel-monitored-metrics');
this.$monitoredMetricsCount = this.$monitoredMetricsPanel.find('.js-monitored-count');
this.$monitoredMetricsLoading = this.$monitoredMetricsPanel.find('.js-loading-metrics');
this.$monitoredMetricsEmpty = this.$monitoredMetricsPanel.find('.js-empty-metrics');
this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list');
this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars');
this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle');
this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count');
this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list');
this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics');
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
} else {
$toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down');
}
}
showMonitoringMetricsPanelState(stateName) {
switch (stateName) {
case PANEL_STATE.LOADING:
this.$monitoredMetricsLoading.removeClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
case PANEL_STATE.LIST:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.removeClass('hidden');
break;
default:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.removeClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
}
}
populateActiveMetrics(metrics) {
let totalMonitoredMetrics = 0;
let totalMissingEnvVarMetrics = 0;
metrics.forEach((metric) => {
this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`);
totalMonitoredMetrics += metric.active_metrics;
if (metric.metrics_missing_requirements > 0) {
this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`);
totalMissingEnvVarMetrics += 1;
}
});
this.$monitoredMetricsCount.text(totalMonitoredMetrics);
this.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
if (totalMissingEnvVarMetrics > 0) {
this.$missingEnvVarPanel.removeClass('hidden');
this.$missingEnvVarPanel.find('.flash-container').off('click');
this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics);
}
}
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
gl.utils.backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint)
.done((res) => {
if (res && res.success) {
stop(res);
} else {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
}
})
.fail(stop);
})
.then((res) => {
if (res && res.data && res.data.length) {
this.populateActiveMetrics(res.data);
} else {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
}
})
.catch(() => {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
});
}
}
...@@ -7,6 +7,11 @@ import Cookies from 'js-cookie'; ...@@ -7,6 +7,11 @@ import Cookies from 'js-cookie';
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners(); this.removeListeners();
this.addEventListeners(); this.addEventListeners();
} }
...@@ -21,14 +26,15 @@ import Cookies from 'js-cookie'; ...@@ -21,14 +26,15 @@ import Cookies from 'js-cookie';
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
const $document = $(document); const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight()); $(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => throttledSetSidebarHeight()); $document.on('scroll', () => debouncedSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
...@@ -207,13 +213,14 @@ import Cookies from 'js-cookie'; ...@@ -207,13 +213,14 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.setSidebarHeight = function() { Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); const $navHeight = this.$navGitlab.outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop(); const diff = $navHeight - $(window).scrollTop();
if (diff > 0) { if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff); this.$rightSidebar.outerHeight($(window).height() - diff);
this.$sidebarInner.height('100%');
} else { } else {
$rightSidebar.outerHeight('100%'); this.$rightSidebar.outerHeight('100%');
this.$sidebarInner.height('');
} }
}; };
......
function expandSectionParent($section, $content) {
$section.addClass('expanded');
$content.off('animationend.expandSectionParent');
}
function expandSection($section) { function expandSection($section) {
$section.find('.js-settings-toggle').text('Close'); $section.find('.js-settings-toggle').text('Collapse');
$section.find('.settings-content').addClass('expanded').off('scroll').scrollTop(0);
const $content = $section.find('.settings-content');
$content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
if ($content.hasClass('no-animate')) {
expandSectionParent($section, $content);
} else {
$content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
}
} }
function closeSection($section) { function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand'); $section.find('.js-settings-toggle').text('Expand');
$section.find('.settings-content').removeClass('expanded').on('scroll', () => expandSection($section));
const $content = $section.find('.settings-content');
$content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
} }
function toggleSection($section) { function toggleSection($section) {
...@@ -21,7 +38,7 @@ function toggleSection($section) { ...@@ -21,7 +38,7 @@ function toggleSection($section) {
export default function initSettingsPanels() { export default function initSettingsPanels() {
$('.settings').each((i, elm) => { $('.settings').each((i, elm) => {
const $section = $(elm); const $section = $(elm);
$section.on('click', '.js-settings-toggle', () => toggleSection($section)); $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll', () => expandSection($section)); $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
}); });
} }
...@@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) { ...@@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) {
} else { } else {
avatar = gon.default_avatar_url; avatar = gon.default_avatar_url;
} }
return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>"; return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>";
}; };
UsersSelect.prototype.formatSelection = function(user) { UsersSelect.prototype.formatSelection = function(user) {
......
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
} }
.landing { .landing {
margin-bottom: $gl-padding; margin: $gl-padding auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
position: relative; position: relative;
......
...@@ -395,6 +395,11 @@ ...@@ -395,6 +395,11 @@
.dropdown-menu-align-right { .dropdown-menu-align-right {
left: auto; left: auto;
right: 0; right: 0;
margin-top: -5px;
@media (max-width: $screen-xs-max) {
left: 0;
}
} }
.dropdown-menu-selectable { .dropdown-menu-selectable {
......
...@@ -236,9 +236,6 @@ ...@@ -236,9 +236,6 @@
width: 35px; width: 35px;
background-color: $white-light; background-color: $white-light;
border: none; border: none;
position: static;
right: 0;
height: 100%;
outline: none; outline: none;
z-index: 1; z-index: 1;
......
...@@ -346,6 +346,7 @@ header { ...@@ -346,6 +346,7 @@ header {
width: auto; width: auto;
min-width: 140px; min-width: 140px;
margin-top: -5px; margin-top: -5px;
left: auto;
.current-user { .current-user {
padding: 5px 18px; padding: 5px 18px;
......
...@@ -45,8 +45,7 @@ ...@@ -45,8 +45,7 @@
li { li {
display: flex; display: flex;
a, a {
.btn-link {
padding: $gl-btn-padding; padding: $gl-btn-padding;
padding-bottom: 11px; padding-bottom: 11px;
font-size: 14px; font-size: 14px;
...@@ -68,29 +67,7 @@ ...@@ -68,29 +67,7 @@
} }
} }
.btn-link { &.active a {
padding-top: 16px;
padding-left: 15px;
padding-right: 15px;
border-left: none;
border-right: none;
border-top: none;
border-radius: 0;
&:hover,
&:active,
&:focus {
background-color: transparent;
}
&:active {
outline: 0;
box-shadow: none;
}
}
&.active a,
&.active .btn-link {
border-bottom: 2px solid $link-underline-blue; border-bottom: 2px solid $link-underline-blue;
color: $black; color: $black;
font-weight: 600; font-weight: 600;
......
...@@ -97,17 +97,19 @@ ...@@ -97,17 +97,19 @@
.issues-bulk-update.right-sidebar { .issues-bulk-update.right-sidebar {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
transition: right $sidebar-transition-duration; width: 0;
right: -$gutter-width; padding: 0;
transition: width $sidebar-transition-duration;
&.right-sidebar-expanded { &.right-sidebar-expanded {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
right: 0; width: $gutter-width;
} }
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
@include maintain-sidebar-dimensions; @include maintain-sidebar-dimensions;
right: -$gutter-width; width: 0;
padding: 0;
.block { .block {
padding: 16px 0; padding: 16px 0;
...@@ -118,5 +120,6 @@ ...@@ -118,5 +120,6 @@
.issuable-sidebar { .issuable-sidebar {
padding: 0 3px; padding: 0 3px;
width: calc(100% + 35px);
} }
} }
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
&:target, &:target,
&.target { &.target {
background: $line-target-blue; background: $line-target-blue;
&.system-note .note-body .note-text.system-note-commit-list::after {
background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%);
}
} }
.avatar { .avatar {
......
...@@ -328,6 +328,7 @@ $note-disabled-comment-color: #b2b2b2; ...@@ -328,6 +328,7 @@ $note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0; $note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3; $note-targe3-inside: #ffffd3;
$note-line2-border: #ddd; $note-line2-border: #ddd;
$note-icon-gutter-width: 55px;
/* /*
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
position: relative; position: relative;
.landing { .landing {
margin-top: 10px; margin-top: 0;
.inner-content { .inner-content {
white-space: normal; white-space: normal;
......
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
.avatar { .avatar {
float: none; float: none;
margin-right: 0;
} }
} }
......
...@@ -146,8 +146,6 @@ table.pipeline-project-metrics tr td { ...@@ -146,8 +146,6 @@ table.pipeline-project-metrics tr td {
} }
.explore-groups.landing { .explore-groups.landing {
margin-top: 10px;
.inner-content { .inner-content {
padding: 0; padding: 0;
......
...@@ -204,7 +204,7 @@ ...@@ -204,7 +204,7 @@
.issuable-sidebar { .issuable-sidebar {
width: calc(100% + 100px); width: calc(100% + 100px);
height: 100%; height: calc(100% - #{$header-height});
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
...@@ -729,36 +729,6 @@ ...@@ -729,36 +729,6 @@
} }
} }
.confidential-issue-warning {
background-color: $gl-gray;
border-radius: 3px;
padding: $gl-btn-padding $gl-padding;
margin-top: $gl-padding-top;
font-size: 14px;
color: $white-light;
.fa {
margin-right: 8px;
}
a {
color: $white-light;
text-decoration: underline;
}
&.affix {
position: static;
width: initial;
@media (min-width: $screen-sm-min) {
position: sticky;
position: -webkit-sticky;
top: 60px;
z-index: 200;
}
}
}
.add-issuable-form-input-wrapper { .add-issuable-form-input-wrapper {
height: auto; height: auto;
padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding; padding: $gl-vert-padding $gl-vert-padding 0 $gl-input-padding;
......
...@@ -142,10 +142,6 @@ ...@@ -142,10 +142,6 @@
width: 250px; width: 250px;
} }
@media (min-width: $screen-md-min) {
width: 350px;
}
&.input-short { &.input-short {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
width: 170px; width: 170px;
......
...@@ -419,7 +419,7 @@ ...@@ -419,7 +419,7 @@
.commit { .commit {
margin: 0; margin: 0;
padding: 10px 0; padding: 10px;
list-style: none; list-style: none;
&:hover { &:hover {
......
...@@ -103,6 +103,42 @@ ...@@ -103,6 +103,42 @@
} }
} }
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 12px;
align-items: center;
@media (max-width: $screen-md-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
.fa {
margin-right: 8px;
}
}
.right-sidebar-expanded {
.confidential-issue-warning {
// When the sidebar is open the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding; padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
...@@ -112,8 +148,20 @@ ...@@ -112,8 +148,20 @@
padding: 6px 0; padding: 6px 0;
} }
.notes-form > li { .notes.notes-form > li.timeline-entry {
@include notes-media('max', $screen-sm-max) {
padding: 0;
}
.timeline-content {
@include notes-media('max', $screen-sm-max) {
margin: 0;
}
}
.timeline-entry-inner {
border: 0; border: 0;
}
} }
.note-edit-form { .note-edit-form {
......
...@@ -14,16 +14,6 @@ ul.notes { ...@@ -14,16 +14,6 @@ ul.notes {
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
@include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
}
.note-created-ago, .note-created-ago,
.note-updated-at { .note-updated-at {
white-space: nowrap; white-space: nowrap;
...@@ -46,15 +36,47 @@ ul.notes { ...@@ -46,15 +36,47 @@ ul.notes {
} }
} }
> li { > li { // .timeline-entry
padding: $gl-padding $gl-btn-padding; padding: 0;
display: block; display: block;
position: relative; position: relative;
border-bottom: 0;
@include notes-media('min', $screen-sm-min) {
padding-left: $note-icon-gutter-width;
}
.timeline-entry-inner {
padding: $gl-padding $gl-btn-padding;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
}
&:last-child { &:target,
// Override `.timeline > li:last-child { border-bottom: none; }` &.target {
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: -1px;
}
.timeline-entry-inner {
border-bottom: 0;
}
}
.timeline-icon {
@include notes-media('min', $screen-sm-min) {
margin-left: -$note-icon-gutter-width;
}
}
.timeline-content {
margin-left: $note-icon-gutter-width;
@include notes-media('min', $screen-sm-min) {
margin-left: 0;
}
} }
&.being-posted { &.being-posted {
...@@ -73,7 +95,7 @@ ul.notes { ...@@ -73,7 +95,7 @@ ul.notes {
} }
&.note-discussion { &.note-discussion {
&.timeline-entry { .timeline-entry-inner {
padding: $gl-padding 10px; padding: $gl-padding 10px;
} }
} }
...@@ -152,13 +174,8 @@ ul.notes { ...@@ -152,13 +174,8 @@ ul.notes {
.system-note { .system-note {
font-size: 14px; font-size: 14px;
padding-left: 0;
clear: both; clear: both;
@include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
.note-header-info { .note-header-info {
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -192,13 +209,16 @@ ul.notes { ...@@ -192,13 +209,16 @@ ul.notes {
.timeline-icon { .timeline-icon {
float: left; float: left;
@include notes-media('min', $screen-sm-min) {
margin-left: 0;
width: auto;
}
svg { svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
fill: $gray-darkest; fill: $gray-darkest;
position: absolute; margin-top: 2px;
left: 0;
top: 2px;
} }
} }
...@@ -250,7 +270,7 @@ ul.notes { ...@@ -250,7 +270,7 @@ ul.notes {
&::after { &::after {
content: ''; content: '';
width: 100%; width: 100%;
height: 67px; height: 70px;
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
...@@ -639,15 +659,12 @@ ul.notes { ...@@ -639,15 +659,12 @@ ul.notes {
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.notes .note { .notes .note {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
padding-left: $gl-padding; padding-left: $gl-padding;
padding-right: $gl-padding; padding-right: $gl-padding;
border-bottom: none;
&.system-note {
padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
} }
} }
} }
......
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
vertical-align: top; vertical-align: middle;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
......
...@@ -388,7 +388,7 @@ a.deploy-project-label { ...@@ -388,7 +388,7 @@ a.deploy-project-label {
padding: 0; padding: 0;
background: transparent; background: transparent;
border: none; border: none;
line-height: 36px; line-height: 34px;
margin: 0; margin: 0;
> li + li::before { > li + li::before {
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
&:first-of-type { &:first-of-type {
margin-top: 10px; margin-top: 10px;
} }
&.expanded {
overflow: visible;
}
} }
.settings-header { .settings-header {
...@@ -147,3 +151,66 @@ ...@@ -147,3 +151,66 @@
margin-left: auto; margin-left: auto;
} }
} }
.prometheus-metrics-monitoring {
.panel {
.panel-toggle {
width: 14px;
}
.badge {
font-size: inherit;
}
.panel-heading .badge-count {
color: $white-light;
background: $common-gray-dark;
}
.panel-body {
padding: 0;
}
.flash-container {
margin-bottom: 0;
cursor: default;
.flash-notice {
border-radius: 0;
}
}
}
.loading-metrics,
.empty-metrics {
padding: 30px 10px;
p,
.btn {
margin-top: 10px;
margin-bottom: 0;
}
}
.loading-metrics .metrics-load-spinner {
color: $loading-color;
}
.metrics-list {
margin-bottom: 0;
li {
padding: $gl-padding;
.badge {
margin-left: 5px;
background: $badge-bg;
}
}
/* Ensure we don't add border if there's only single li */
li + li {
border-top: 1px solid $border-color;
}
}
}
.tree-holder { .tree-holder {
> .nav-block { .nav-block {
margin: 11px 0; margin: 10px 0;
@media (min-width: $screen-sm-min) {
display: flex;
.tree-ref-container {
flex: 1;
}
.tree-controls {
text-align: right;
.btn-group {
margin-left: 10px;
}
}
.tree-ref-holder {
float: left;
margin-right: 15px;
}
.repo-breadcrumb {
li:last-of-type {
position: relative;
}
}
.add-to-tree-dropdown {
position: absolute;
left: 18px;
}
}
}
@media (max-width: $screen-xs-max) {
.repo-breadcrumb {
margin-top: 10px;
position: relative;
.dropdown-menu {
min-width: 100%;
width: 100%;
left: inherit;
right: 0;
}
}
.add-to-tree-dropdown {
position: absolute;
left: 0;
right: 0;
}
.tree-controls {
margin-bottom: 10px;
.btn,
.dropdown,
.btn-group {
width: 100%;
}
.btn {
margin: 10px 0 0;
}
}
} }
.file-finder { .file-finder {
...@@ -131,11 +197,6 @@ ...@@ -131,11 +197,6 @@
} }
} }
.tree-ref-holder {
float: left;
margin-right: 15px;
}
.blob-commit-info { .blob-commit-info {
list-style: none; list-style: none;
margin: 0; margin: 0;
...@@ -159,16 +220,6 @@ ...@@ -159,16 +220,6 @@
color: $md-link-color; color: $md-link-color;
} }
.tree-controls {
float: right;
position: relative;
z-index: 2;
.project-action-button {
margin-left: $btn-side-margin;
}
}
.repo-charts { .repo-charts {
.sub-header { .sub-header {
margin: 20px 0; margin: 20px 0;
......
...@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base ...@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
render_404 render_404
end end
rescue_from(ActionController::UnknownFormat) do
render_404
end
rescue_from Gitlab::Access::AccessDeniedError do |exception| rescue_from Gitlab::Access::AccessDeniedError do |exception|
render_403 render_403
end end
......
...@@ -57,6 +57,11 @@ class Projects::ApplicationController < ApplicationController ...@@ -57,6 +57,11 @@ class Projects::ApplicationController < ApplicationController
render_404 unless project.feature_available?(feature, current_user) render_404 unless project.feature_available?(feature, current_user)
end end
def check_issuables_available!
render_404 unless project.feature_available?(:issues, current_user) ||
project.feature_available?(:merge_requests, current_user)
end
def method_missing(method_sym, *arguments, &block) def method_missing(method_sym, *arguments, &block)
case method_sym.to_s case method_sym.to_s
when /\Aauthorize_(.*)!\z/ when /\Aauthorize_(.*)!\z/
......
...@@ -6,7 +6,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :authorize_update_deploy_key!, only: [:edit, :update] before_action :authorize_update_deploy_key!, only: [:edit, :update]
layout "project_settings" layout 'project_settings'
def index def index
respond_to do |format| respond_to do |format|
...@@ -73,7 +73,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -73,7 +73,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
protected protected
def deploy_key def deploy_key
@deploy_key ||= @project.deploy_keys.find(params[:id]) @deploy_key ||= DeployKey.find(params[:id])
end end
def create_params def create_params
......
...@@ -22,6 +22,22 @@ class Projects::DeploymentsController < Projects::ApplicationController ...@@ -22,6 +22,22 @@ class Projects::DeploymentsController < Projects::ApplicationController
render_404 render_404
end end
def additional_metrics
return render_404 unless deployment.has_additional_metrics?
respond_to do |format|
format.json do
metrics = deployment.additional_metrics
if metrics.any?
render json: metrics
else
head :no_content
end
end
end
end
private private
def deployment def deployment
......
class Projects::DiscussionsController < Projects::ApplicationController class Projects::DiscussionsController < Projects::ApplicationController
before_action :module_enabled before_action :check_merge_requests_available!
before_action :merge_request before_action :merge_request
before_action :discussion before_action :discussion
before_action :authorize_resolve_discussion! before_action :authorize_resolve_discussion!
...@@ -34,8 +34,4 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -34,8 +34,4 @@ class Projects::DiscussionsController < Projects::ApplicationController
def authorize_resolve_discussion! def authorize_resolve_discussion!
access_denied! unless discussion.can_resolve?(current_user) access_denied! unless discussion.can_resolve?(current_user)
end end
def module_enabled
render_404 unless @project.feature_available?(:merge_requests, current_user)
end
end end
...@@ -16,8 +16,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -16,8 +16,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { render json: {
environments: EnvironmentSerializer environments: EnvironmentSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
...@@ -151,6 +149,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -151,6 +149,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def additional_metrics
respond_to do |format|
format.json do
additional_metrics = environment.additional_metrics || {}
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
end
end
end
private private
def verify_api_request! def verify_api_request!
......
...@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -11,7 +11,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new, :export_csv] prepend_before_action :authenticate_user!, only: [:new, :export_csv]
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv] before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
# Allow write(create) issue # Allow write(create) issue
...@@ -253,7 +253,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -253,7 +253,7 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end end
def module_enabled def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker? return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
end end
......
class Projects::LabelsController < Projects::ApplicationController class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
before_action :module_enabled before_action :check_issuables_available!
before_action :label, only: [:edit, :update, :destroy, :promote] before_action :label, only: [:edit, :update, :destroy, :promote]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription] before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label! before_action :authorize_read_label!
...@@ -135,12 +135,6 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -135,12 +135,6 @@ class Projects::LabelsController < Projects::ApplicationController
protected protected
def module_enabled
unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
def label_params def label_params
params.require(:label).permit(:title, :description, :color) params.require(:label).permit(:title, :description, :color)
end end
......
...@@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include IssuableCollections include IssuableCollections
before_action :module_enabled before_action :check_merge_requests_available!
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge,
:pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds,
...@@ -520,10 +520,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -520,10 +520,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user) return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end end
def module_enabled
return render_404 unless @project.feature_available?(:merge_requests, current_user)
end
def validates_merge_request def validates_merge_request
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
...@@ -647,10 +643,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -647,10 +643,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request_params def merge_request_params
params.require(:merge_request) params.require(:merge_request)
.permit(merge_request_params_ce << merge_request_params_ee) .permit(merge_request_params_attributes)
end end
def merge_request_params_ce def merge_request_params_attributes
[ [
:assignee_id, :assignee_id,
:description, :description,
...@@ -666,7 +662,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -666,7 +662,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:title, :title,
label_ids: [] label_ids: []
] ] + merge_request_params_ee
end end
def merge_request_params_ee def merge_request_params_ee
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions include MilestoneActions
before_action :module_enabled before_action :check_issuables_available!
before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels] before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels]
# Allow read any milestone # Allow read any milestone
...@@ -96,12 +96,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -96,12 +96,6 @@ class Projects::MilestonesController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_milestone, @project) return render_404 unless can?(current_user, :admin_milestone, @project)
end end
def module_enabled
unless @project.feature_available?(:issues, current_user) || @project.feature_available?(:merge_requests, current_user)
return render_404
end
end
def milestone_params def milestone_params
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end end
......
class Projects::PrometheusController < Projects::ApplicationController
before_action :authorize_read_project!
before_action :require_prometheus_metrics!
def active_metrics
respond_to do |format|
format.json do
matched_metrics = project.prometheus_service.matched_metrics || {}
if matched_metrics.any?
render json: matched_metrics
else
head :no_content
end
end
end
end
private
def require_prometheus_metrics!
render_404 unless project.prometheus_service.present?
end
end
...@@ -5,7 +5,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -5,7 +5,7 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions include SnippetsActions
include RendersBlob include RendersBlob
before_action :module_enabled before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
# Allow read any snippet # Allow read any snippet
...@@ -102,10 +102,6 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -102,10 +102,6 @@ class Projects::SnippetsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_project_snippet, @snippet) return render_404 unless can?(current_user, :admin_project_snippet, @snippet)
end end
def module_enabled
return render_404 unless @project.feature_available?(:snippets, current_user)
end
def snippet_params def snippet_params
params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description)
end end
......
...@@ -85,20 +85,20 @@ module CommitsHelper ...@@ -85,20 +85,20 @@ module CommitsHelper
if @path.blank? if @path.blank?
return link_to( return link_to(
"Browse Files", _("Browse Files"),
namespace_project_tree_path(project.namespace, project, commit), namespace_project_tree_path(project.namespace, project, commit),
class: "btn btn-default" class: "btn btn-default"
) )
elsif @repo.blob_at(commit.id, @path) elsif @repo.blob_at(commit.id, @path)
return link_to( return link_to(
"Browse File", _("Browse File"),
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "btn btn-default" class: "btn btn-default"
) )
elsif @path.present? elsif @path.present?
return link_to( return link_to(
"Browse Directory", _("Browse Directory"),
namespace_project_tree_path(project.namespace, project, namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)), tree_join(commit.id, @path)),
class: "btn btn-default" class: "btn btn-default"
......
...@@ -138,8 +138,8 @@ module IssuablesHelper ...@@ -138,8 +138,8 @@ module IssuablesHelper
end end
output << "&ensp;".html_safe output << "&ensp;".html_safe
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
output output
end end
...@@ -227,7 +227,8 @@ module IssuablesHelper ...@@ -227,7 +227,8 @@ module IssuablesHelper
initialTitleHtml: markdown_field(issuable, :title), initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title, initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description), initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description initialDescriptionText: issuable.description,
initialTaskStatus: issuable.task_status
} }
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
......
...@@ -4,4 +4,14 @@ module UsersHelper ...@@ -4,4 +4,14 @@ module UsersHelper
title: user.email, title: user.email,
class: 'has-tooltip commit-committer-link') class: 'has-tooltip commit-committer-link')
end end
def user_email_help_text(user)
return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present?
confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
h('Please click the link in the confirmation email before continuing. It was sent to ') +
content_tag(:strong) { user.unconfirmed_email } + h('.') +
content_tag(:p) { confirmation_link }
end
end end
...@@ -142,17 +142,6 @@ module Ci ...@@ -142,17 +142,6 @@ module Ci
ExpandVariables.expand(environment, simple_variables) if environment ExpandVariables.expand(environment, simple_variables) if environment
end end
def environment_url
return @environment_url if defined?(@environment_url)
@environment_url =
if unexpanded_url = options&.dig(:environment, :url)
ExpandVariables.expand(unexpanded_url, simple_variables)
else
persisted_environment&.external_url
end
end
def has_environment? def has_environment?
environment.present? environment.present?
end end
...@@ -196,7 +185,7 @@ module Ci ...@@ -196,7 +185,7 @@ module Ci
slugified.gsub(/[^a-z0-9]/, '-')[0..62] slugified.gsub(/[^a-z0-9]/, '-')[0..62]
end end
# Variables whose value does not depend on other variables # Variables whose value does not depend on environment
def simple_variables def simple_variables
variables = predefined_variables variables = predefined_variables
variables += project.predefined_variables variables += project.predefined_variables
...@@ -211,7 +200,8 @@ module Ci ...@@ -211,7 +200,8 @@ module Ci
variables variables
end end
# All variables, including those dependent on other variables # All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables def variables
simple_variables.concat(persisted_environment_variables) simple_variables.concat(persisted_environment_variables)
end end
...@@ -500,9 +490,10 @@ module Ci ...@@ -500,9 +490,10 @@ module Ci
variables = persisted_environment.predefined_variables variables = persisted_environment.predefined_variables
if url = environment_url # Here we're passing unexpanded environment_url for runner to expand,
variables << { key: 'CI_ENVIRONMENT_URL', value: url, public: true } # and we need to make sure that CI_ENVIRONMENT_NAME and
end # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url
variables variables
end end
...@@ -525,6 +516,10 @@ module Ci ...@@ -525,6 +516,10 @@ module Ci
variables variables
end end
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
def build_attributes_from_config def build_attributes_from_config
return {} unless pipeline.config_processor return {} unless pipeline.config_processor
......
...@@ -107,6 +107,14 @@ module Routable ...@@ -107,6 +107,14 @@ module Routable
RequestStore[key] ||= uncached_full_path RequestStore[key] ||= uncached_full_path
end end
def build_full_path
if parent && path
parent.full_path + '/' + path
else
path
end
end
private private
def uncached_full_path def uncached_full_path
...@@ -135,14 +143,6 @@ module Routable ...@@ -135,14 +143,6 @@ module Routable
end end
end end
def build_full_path
if parent && path
parent.full_path + '/' + path
else
path
end
end
def update_route def update_route
prepare_route prepare_route
route.save route.save
......
...@@ -114,6 +114,17 @@ class Deployment < ActiveRecord::Base ...@@ -114,6 +114,17 @@ class Deployment < ActiveRecord::Base
project.monitoring_service.deployment_metrics(self) project.monitoring_service.deployment_metrics(self)
end end
def has_additional_metrics?
project.prometheus_service.present?
end
def additional_metrics
return {} unless project.prometheus_service.present?
metrics = project.prometheus_service.additional_deployment_metrics(self)
metrics&.merge(deployment_time: created_at.to_i) || {}
end
private private
def ref_path def ref_path
......
...@@ -161,6 +161,16 @@ class Environment < ActiveRecord::Base ...@@ -161,6 +161,16 @@ class Environment < ActiveRecord::Base
project.monitoring_service.environment_metrics(self) if has_metrics? project.monitoring_service.environment_metrics(self) if has_metrics?
end end
def has_additional_metrics?
project.prometheus_service.present? && available? && last_deployment.present?
end
def additional_metrics
if has_additional_metrics?
project.prometheus_service.additional_environment_metrics(self)
end
end
# An environment name is not necessarily suitable for use in URLs, DNS # An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has # or other third-party contexts, so provide a slugified version. A slug has
# the following properties: # the following properties:
......
...@@ -70,7 +70,7 @@ module ChatMessage ...@@ -70,7 +70,7 @@ module ChatMessage
end end
def branch_link def branch_link
"`[#{ref}](#{branch_url})`" "[#{ref}](#{branch_url})"
end end
def project_link def project_link
......
...@@ -61,7 +61,7 @@ module ChatMessage ...@@ -61,7 +61,7 @@ module ChatMessage
end end
def removed_branch_message def removed_branch_message
"#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" "#{user_name} removed #{ref_type} #{ref} from #{project_link}"
end end
def push_message def push_message
...@@ -102,7 +102,7 @@ module ChatMessage ...@@ -102,7 +102,7 @@ module ChatMessage
end end
def branch_link def branch_link
"`[#{ref}](#{branch_url})`" "[#{ref}](#{branch_url})"
end end
def project_link def project_link
......
...@@ -28,17 +28,6 @@ class PrometheusService < MonitoringService ...@@ -28,17 +28,6 @@ class PrometheusService < MonitoringService
'Prometheus monitoring' 'Prometheus monitoring'
end end
def help
<<-MD.strip_heredoc
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
and `container_memory_usage_bytes` from the configured Prometheus server.
If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
or have set up your own Prometheus server, an `environment` label is required on each metric to
[identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels).
MD
end
def self.to_param def self.to_param
'prometheus' 'prometheus'
end end
...@@ -50,6 +39,7 @@ class PrometheusService < MonitoringService ...@@ -50,6 +39,7 @@ class PrometheusService < MonitoringService
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
required: true required: true
} }
] ]
...@@ -65,23 +55,34 @@ class PrometheusService < MonitoringService ...@@ -65,23 +55,34 @@ class PrometheusService < MonitoringService
end end
def environment_metrics(environment) def environment_metrics(environment)
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself) with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics))
end end
def deployment_metrics(deployment) def deployment_metrics(deployment)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself) metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics))
metrics&.merge(deployment_time: created_at.to_i) || {} metrics&.merge(deployment_time: created_at.to_i) || {}
end end
def additional_environment_metrics(environment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself)
end
def additional_deployment_metrics(deployment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself)
end
def matched_metrics
with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself)
end
# Cache metrics for specific environment # Cache metrics for specific environment
def calculate_reactive_cache(query_class_name, *args) def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
metrics = Kernel.const_get(query_class_name).new(client).query(*args) data = Kernel.const_get(query_class_name).new(client).query(*args)
{ {
success: true, success: true,
metrics: metrics, data: data,
last_update: Time.now.utc last_update: Time.now.utc
} }
rescue Gitlab::PrometheusError => err rescue Gitlab::PrometheusError => err
...@@ -91,4 +92,11 @@ class PrometheusService < MonitoringService ...@@ -91,4 +92,11 @@ class PrometheusService < MonitoringService
def client def client
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) @prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
end end
private
def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data
metrics
end
end end
...@@ -2,7 +2,7 @@ class CreateDeploymentService ...@@ -2,7 +2,7 @@ class CreateDeploymentService
attr_reader :job attr_reader :job
delegate :expanded_environment_name, delegate :expanded_environment_name,
:environment_url, :variables,
:project, :project,
to: :job to: :job
...@@ -14,7 +14,8 @@ class CreateDeploymentService ...@@ -14,7 +14,8 @@ class CreateDeploymentService
return unless executable? return unless executable?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
environment.external_url = environment_url if environment_url environment.external_url = expanded_environment_url if
expanded_environment_url
environment.fire_state_event(action) environment.fire_state_event(action)
return unless environment.save return unless environment.save
...@@ -49,6 +50,17 @@ class CreateDeploymentService ...@@ -49,6 +50,17 @@ class CreateDeploymentService
@environment_options ||= job.options&.dig(:environment) || {} @environment_options ||= job.options&.dig(:environment) || {}
end end
def expanded_environment_url
return @expanded_environment_url if defined?(@expanded_environment_url)
@expanded_environment_url =
ExpandVariables.expand(environment_url, variables) if environment_url
end
def environment_url
environment_options[:url]
end
def on_stop def on_stop
environment_options[:on_stop] environment_options[:on_stop]
end end
......
...@@ -12,87 +12,121 @@ module Projects ...@@ -12,87 +12,121 @@ module Projects
TransferError = Class.new(StandardError) TransferError = Class.new(StandardError)
def execute(new_namespace) def execute(new_namespace)
if new_namespace.blank? @new_namespace = new_namespace
if @new_namespace.blank?
raise TransferError, 'Please select a new namespace for your project.' raise TransferError, 'Please select a new namespace for your project.'
end end
unless allowed_transfer?(current_user, project, new_namespace)
unless allowed_transfer?(current_user, project)
raise TransferError, 'Transfer failed, please contact an admin.' raise TransferError, 'Transfer failed, please contact an admin.'
end end
transfer(project, new_namespace)
transfer(project)
true
rescue Projects::TransferService::TransferError => ex rescue Projects::TransferService::TransferError => ex
project.reload project.reload
project.errors.add(:new_namespace, ex.message) project.errors.add(:new_namespace, ex.message)
false false
end end
def transfer(project, new_namespace) private
old_namespace = project.namespace
Project.transaction do def transfer(project)
old_path = project.path_with_namespace @old_path = project.path_with_namespace
old_group = project.group @old_group = project.group
new_path = File.join(new_namespace.try(:full_path) || '', project.path) @new_path = File.join(@new_namespace.try(:full_path) || '', project.path)
@old_namespace = project.namespace
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists?
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
if project.has_container_registry_tags? if project.has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry # We currently don't support renaming repository if it contains tags in container registry
raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
end end
project.expire_caches_before_rename(old_path) attempt_transfer_transaction
end
def attempt_transfer_transaction
Project.transaction do
project.expire_caches_before_rename(@old_path)
# Apply new namespace id and visibility level update_namespace_and_visibility(@new_namespace)
project.namespace = new_namespace
project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
# Notifications # Notifications
project.send_move_instructions(old_path) project.send_move_instructions(@old_path)
# Move main repository # Move main repository
unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) unless move_repo_folder(@old_path, @new_path)
raise TransferError.new('Cannot move project') raise TransferError.new('Cannot move project')
end end
# Move wiki repo also if present # Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
# Move missing group labels to project # Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads # Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
# Move pages # Move pages
Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = old_path project.old_path_with_namespace = @old_path
SystemHooksService.new.execute_hooks_for(project, :transfer) execute_system_hooks
end end
rescue Exception # rubocop:disable Lint/RescueException
refresh_permissions(old_namespace, new_namespace) rollback_side_effects
raise
true ensure
refresh_permissions
end end
def allowed_transfer?(current_user, project, namespace) def allowed_transfer?(current_user, project)
namespace && @new_namespace &&
can?(current_user, :change_namespace, project) && can?(current_user, :change_namespace, project) &&
namespace.id != project.namespace_id && @new_namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace) current_user.can?(:create_projects, @new_namespace)
end end
def refresh_permissions(old_namespace, new_namespace) def update_namespace_and_visibility(to_namespace)
# Apply new namespace id and visibility level
project.namespace = to_namespace
project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
end
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to # This ensures we only schedule 1 job for every user that has access to
# the namespaces. # the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations | user_ids = @old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations @new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute UserProjectAccessChangedService.new(user_ids).execute
end end
def rollback_side_effects
rollback_folder_move
update_namespace_and_visibility(@old_namespace)
end
def rollback_folder_move
move_repo_folder(@new_path, @old_path)
move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki")
end
def move_repo_folder(from_name, to_name)
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
end
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
end end
end end
...@@ -26,7 +26,7 @@ class DynamicPathValidator < ActiveModel::EachValidator ...@@ -26,7 +26,7 @@ class DynamicPathValidator < ActiveModel::EachValidator
end end
def path_valid_for_record?(record, value) def path_valid_for_record?(record, value)
full_path = record.respond_to?(:full_path) ? record.full_path : value full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value
return true unless full_path return true unless full_path
......
...@@ -353,6 +353,10 @@ ...@@ -353,6 +353,10 @@
= f.label :prometheus_metrics_enabled do = f.label :prometheus_metrics_enabled do
= f.check_box :prometheus_metrics_enabled = f.check_box :prometheus_metrics_enabled
Enable Prometheus Metrics Enable Prometheus Metrics
- unless Gitlab::Metrics.metrics_folder_present?
.help-block
%strong.cred WARNING:
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
%fieldset %fieldset
%legend Background Jobs %legend Background Jobs
......
...@@ -18,5 +18,4 @@ ...@@ -18,5 +18,4 @@
- if current_user - if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
.prepend-top-default
= render 'shared/merge_requests' = render 'shared/merge_requests'
= render 'profiles/head' = render 'profiles/head'
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f|
= form_errors(@user) = form_errors(@user)
.row .row
...@@ -11,11 +11,11 @@ ...@@ -11,11 +11,11 @@
- if @user.avatar? - if @user.avatar?
You can change your avatar here You can change your avatar here
- if gravatar_enabled? - if gravatar_enabled?
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
- else - else
You can upload an avatar here You can upload an avatar here
- if gravatar_enabled? - if gravatar_enabled?
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
.col-lg-9 .col-lg-9
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
...@@ -26,12 +26,12 @@ ...@@ -26,12 +26,12 @@
%a.btn.js-choose-user-avatar-button %a.btn.js-choose-user-avatar-button
Browse file... Browse file...
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
= f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*" = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.help-block .help-block
The maximum file size allowed is 200KB. The maximum file size allowed is 200KB.
- if @user.avatar? - if @user.avatar?
%hr %hr
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray" = link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray'
%hr %hr
.row .row
.col-lg-3.profile-settings-sidebar .col-lg-3.profile-settings-sidebar
...@@ -43,91 +43,50 @@ ...@@ -43,91 +43,50 @@
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.col-lg-9 .col-lg-9
.row .row
.form-group.col-md-9 = f.text_field :name, required: true, wrapper: { class: 'col-md-9' },
= f.label :name, class: "label-light" help: 'Enter your name, so people you know can recognize you.'
= f.text_field :name, class: "form-control", required: true = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
%span.help-block Enter your name, so people you know can recognize you.
.form-group.col-md-3
= f.label :id, class: 'label-light' do
User ID
= f.text_field :id, class: 'form-control', readonly: true
.form-group
= f.label :email, class: "label-light"
- if @user.external_email? - if @user.external_email?
= f.text_field :email, class: "form-control", required: true, readonly: true = f.text_field :email, required: true, readonly: true, help: 'Your email address was automatically set based on your #{email_provider_label} account.'
%span.help-block.light
Your email address was automatically set based on your #{email_provider_label} account.
- else
- if @user.temp_oauth_email?
= f.text_field :email, class: "form-control", required: true, value: nil
- else
= f.text_field :email, class: "form-control", required: true
- if @user.unconfirmed_email.present?
%span.help-block
Please click the link in the confirmation email before continuing. It was sent to
= succeed "." do
%strong= @user.unconfirmed_email
%p
= link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
- else - else
%span.help-block We also use email for avatar detection if no avatar is uploaded. = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
.form-group help: user_email_help_text(@user)
= f.label :public_email, class: "label-light" = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2" { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' },
%span.help-block This email will be displayed on your public profile. control_class: 'select2'
.form-group
= f.label :preferred_language, class: "label-light"
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{}, class: "select2" { help: 'This feature is experimental and translations are not complete yet.' },
%span.help-block This feature is experimental and translations are not complete yet. control_class: 'select2'
.form-group = f.text_field :skype
= f.label :skype, class: "label-light" = f.text_field :linkedin
= f.text_field :skype, class: "form-control" = f.text_field :twitter
.form-group = f.text_field :website_url, label: 'Website'
= f.label :linkedin, class: "label-light" = f.text_field :location
= f.text_field :linkedin, class: "form-control" = f.text_field :organization
.form-group = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
= f.label :twitter, class: "label-light"
= f.text_field :twitter, class: "form-control"
.form-group
= f.label :website_url, 'Website', class: "label-light"
= f.text_field :website_url, class: "form-control"
.form-group
= f.label :location, 'Location', class: "label-light"
= f.text_field :location, class: "form-control"
.form-group
= f.label :organization, 'Organization', class: "label-light"
= f.text_field :organization, class: "form-control"
.form-group
= f.label :bio, class: "label-light"
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
%span.help-block Tell us about yourself in fewer than 250 characters.
.prepend-top-default.append-bottom-default .prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: "btn btn-success" = f.submit 'Update profile settings', class: 'btn btn-success'
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel" = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel'
.modal.modal-profile-crop .modal.modal-profile-crop
.modal-dialog .modal-dialog
.modal-content .modal-content
.modal-header .modal-header
%button.close{ :type => "button", :'data-dismiss' => "modal" } %button.close{ type: 'button', 'data-dismiss': 'modal' }
%span %span
&times; &times;
%h4.modal-title %h4.modal-title
Position and size your new avatar Position and size your new avatar
.modal-body .modal-body
.profile-crop-image-container .profile-crop-image-container
%img.modal-profile-crop-image{ alt: "Avatar cropper" } %img.modal-profile-crop-image{ alt: 'Avatar cropper' }
.crop-controls .crop-controls
.btn-group .btn-group
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
%span.fa.fa-search-plus %span.fa.fa-search-plus
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } %button.btn.btn-primary{ data: { method: 'zoom', option: '-0.1' } }
%span.fa.fa-search-minus %span.fa.fa-search-minus
.modal-footer .modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ :type => "button" } %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
Set new profile picture Set new profile picture
= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do = link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn shortcuts-find-file', rel: 'nofollow' do
= icon('search') = icon('search')
%span= _('Find file') %span= _('Find file')
...@@ -9,6 +9,12 @@ ...@@ -9,6 +9,12 @@
%li %li
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
- if defined?(@issue) && @issue.confidential?
%li.confidential-issue-warning
= icon('warning')
%span This is a confidential issue. Your comment will not be visible to the public.
%li.pull-right %li.pull-right
.toolbar-group .toolbar-group
= markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
......
- blame = local_assigns.fetch(:blame, false) - blame = local_assigns.fetch(:blame, false)
.nav-block .nav-block
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
%ul.breadcrumb.repo-breadcrumb
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li
- if path == @path
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
%strong= title
- else
= link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
.tree-controls .tree-controls
= render 'projects/find_file_link' = render 'projects/find_file_link'
.btn-group.prepend-left-10{ role: "group" }< .btn-group{ role: "group" }<
-# only show normal/blame view links for text files -# only show normal/blame view links for text files
- if blob.readable_text? - if blob.readable_text?
- if blame - if blame
...@@ -18,19 +35,3 @@ ...@@ -18,19 +35,3 @@
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url' tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url'
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
%ul.breadcrumb.repo-breadcrumb
%li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li
- if path == @path
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
%strong= title
- else
= link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.dropzone .dropzone
.dropzone-previews.blob-upload-dropzone-previews .dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light %p.dz-message.light
- upload_link = link_to n_('UploadLink|click to upload'), '#', class: "markdown-selector" - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector"
- dropzone_text = _('Attach a file by drag &amp; drop or %{upload_link}') % { upload_link: upload_link } - dropzone_text = _('Attach a file by drag &amp; drop or %{upload_link}') % { upload_link: upload_link }
#{ dropzone_text.html_safe } #{ dropzone_text.html_safe }
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
= label_tag 'start_branch', branch_label, class: 'control-label' = label_tag 'start_branch', branch_label, class: 'control-label'
.col-sm-10 .col-sm-10
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
= dropdown_tag(@project.default_branch, options: { title: n_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: n_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= render 'shared/new_merge_request_checkbox' = render 'shared/new_merge_request_checkbox'
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header.js-commit-header{ data: { day: day } } %li.commit-header.js-commit-header{ data: { day: day } }
%span.day= day.strftime('%d %b, %Y') %span.day= l(day, format: '%d %b, %Y')
%span.commits-count= pluralize(commits.count, 'commit') %span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
%li.commits-row{ data: { day: day } } %li.commits-row{ data: { day: day } }
%ul.content-list.commit-list %ul.content-list.commit-list
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
- if hidden > 0 - if hidden > 0
%li.alert.alert-warning %li.alert.alert-warning
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. = n_('%d additional commit has been omitted to prevent performance issues.', '%d additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- @no_container = true - @no_container = true
- page_title "Commits", @ref - page_title _("Commits"), @ref
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
...@@ -18,16 +18,16 @@ ...@@ -18,16 +18,16 @@
.block-controls.hidden-xs.hidden-sm .block-controls.hidden-xs.hidden-sm
- if @merge_request.present? - if @merge_request.present?
.control .control
= link_to "View open merge request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' = link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref) - elsif create_mr_button?(@repository.root_ref, @ref)
.control .control
= link_to "Create merge request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' = link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control .control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
= search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control .control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits feed", class: 'btn' do = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss") = icon("rss")
= render 'projects/commits/mirror_status' = render 'projects/commits/mirror_status'
......
...@@ -3,14 +3,15 @@ ...@@ -3,14 +3,15 @@
.table-mobile-header{ role: 'rowheader' } ID .table-mobile-header{ role: 'rowheader' } ID
%strong.table-mobile-content ##{deployment.iid} %strong.table-mobile-content ##{deployment.iid}
.table-section.section-40{ role: 'gridcell' } .table-section.section-30{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Commit .table-mobile-header{ role: 'rowheader' } Commit
= render 'projects/deployments/commit', deployment: deployment = render 'projects/deployments/commit', deployment: deployment
.table-section.section-15.build-column{ role: 'gridcell' } .table-section.section-25.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Job .table-mobile-header{ role: 'rowheader' } Job
- if deployment.deployable - if deployment.deployable
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link table-mobile-content' do .table-mobile-content
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id}) #{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.user - if deployment.user
by by
...@@ -21,6 +22,6 @@ ...@@ -21,6 +22,6 @@
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' } .table-section.section-20.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-button .btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment
- @content_class = "limit-container-width" unless fluid_layout
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('service_desk') = webpack_bundle_tag('service_desk')
...@@ -6,10 +8,10 @@ ...@@ -6,10 +8,10 @@
.project-edit-container .project-edit-container
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Project settings Project settings
.col-lg-9 .col-lg-8
.project-edit-errors .project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset %fieldset
...@@ -53,66 +55,66 @@ ...@@ -53,66 +55,66 @@
Sharing &amp; Permissions Sharing &amp; Permissions
.form_group.prepend-top-20.sharing-and-permissions .form_group.prepend-top-20.sharing-and-permissions
.row.js-visibility-select .row.js-visibility-select
.col-md-9 .col-md-8
%label.label-light .label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to icon('question-circle'), help_page_path("public_access/public_access") = link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block %span.help-block
.col-md-3.visibility-select-container .col-md-4.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields| = f.fields_for :project_feature do |feature_fields|
%fieldset.features %fieldset.features
.row .row
.col-md-9.project-feature .col-md-8.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light' = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block View and edit files in this project %span.help-block View and edit files in this project
.col-md-3.js-repo-access-level .col-md-4.js-repo-access-level
= project_feature_access_select(:repository_access_level) = project_feature_access_select(:repository_access_level)
.row .row
.col-md-9.project-feature.nested .col-md-8.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream %span.help-block Submit changes to be merged upstream
.col-md-3 .col-md-4
= project_feature_access_select(:merge_requests_access_level) = project_feature_access_select(:merge_requests_access_level)
.row .row
.col-md-9.project-feature.nested .col-md-8.project-feature.nested
= feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Build, test, and deploy your changes %span.help-block Build, test, and deploy your changes
.col-md-3 .col-md-4
= project_feature_access_select(:builds_access_level) = project_feature_access_select(:builds_access_level)
.row .row
.col-md-9.project-feature .col-md-8.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository %span.help-block Share code pastes with others out of Git repository
.col-md-3 .col-md-4
= project_feature_access_select(:snippets_access_level) = project_feature_access_select(:snippets_access_level)
.row .row
.col-md-9.project-feature .col-md-8.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light' = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project %span.help-block Lightweight issue tracking system for this project
.col-md-3 .col-md-4
= project_feature_access_select(:issues_access_level) = project_feature_access_select(:issues_access_level)
.row .row
.col-md-9.project-feature .col-md-8.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation %span.help-block Pages for project documentation
.col-md-3 .col-md-4
= project_feature_access_select(:wiki_access_level) = project_feature_access_select(:wiki_access_level)
.form-group .form-group
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin? - if Gitlab.config.lfs.enabled && current_user.admin?
.row.js-lfs-enabled .row.js-lfs-enabled
.col-md-9 .col-md-8
= f.label :lfs_enabled, 'LFS', class: 'label-light' = f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block %span.help-block
Git Large File Storage Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3 .col-md-4
.select-wrapper .select-wrapper
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
= icon('chevron-down') = icon('chevron-down')
...@@ -164,19 +166,19 @@ ...@@ -164,19 +166,19 @@
.row.prepend-top-default .row.prepend-top-default
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Housekeeping Housekeeping
%p.append-bottom-0 %p.append-bottom-0
%p %p
Runs a number of housekeeping tasks within the current repository, Runs a number of housekeeping tasks within the current repository,
such as compressing file revisions and removing unreachable objects. such as compressing file revisions and removing unreachable objects.
.col-lg-9 .col-lg-8
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default" method: :post, class: "btn btn-default"
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Export project Export project
%p.append-bottom-0 %p.append-bottom-0
...@@ -185,7 +187,7 @@ ...@@ -185,7 +187,7 @@
%p %p
Once the exported file is ready, you will receive a notification email with a download link. Once the exported file is ready, you will receive a notification email with a download link.
.col-lg-9 .col-lg-8
- if @project.export_project_path - if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
...@@ -216,7 +218,7 @@ ...@@ -216,7 +218,7 @@
- if can? current_user, :archive_project, @project - if can? current_user, :archive_project, @project
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.warning-title.prepend-top-0 %h4.warning-title.prepend-top-0
- if @project.archived? - if @project.archived?
Unarchive project Unarchive project
...@@ -227,7 +229,7 @@ ...@@ -227,7 +229,7 @@
Unarchiving the project will mark its repository as active. The project can be committed to. Unarchiving the project will mark its repository as active. The project can be committed to.
- else - else
Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
.col-lg-9 .col-lg-8
- if @project.archived? - if @project.archived?
%p %p
%strong Once active this project shows up in the search and on the dashboard. %strong Once active this project shows up in the search and on the dashboard.
...@@ -242,10 +244,10 @@ ...@@ -242,10 +244,10 @@
method: :post, class: "btn btn-warning" method: :post, class: "btn btn-warning"
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0.warning-title %h4.prepend-top-0.warning-title
Rename repository Rename repository
.col-lg-9 .col-lg-8
= render 'projects/errors' = render 'projects/errors'
= form_for([@project.namespace.becomes(Namespace), @project]) do |f| = form_for([@project.namespace.becomes(Namespace), @project]) do |f|
.form-group.project_name_holder .form-group.project_name_holder
...@@ -270,12 +272,12 @@ ...@@ -270,12 +272,12 @@
- if can?(current_user, :change_namespace, @project) - if can?(current_user, :change_namespace, @project)
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
Transfer project to new group Transfer project to new group
%p.append-bottom-0 %p.append-bottom-0
Please select the group you want to transfer this project to in the dropdown to the right. Please select the group you want to transfer this project to in the dropdown to the right.
.col-lg-9 .col-lg-8
= form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f|
.form-group .form-group
= label_tag :new_namespace_id, nil, class: 'label-light' do = label_tag :new_namespace_id, nil, class: 'label-light' do
...@@ -291,7 +293,7 @@ ...@@ -291,7 +293,7 @@
- if @project.forked? && can?(current_user, :remove_fork_project, @project) - if @project.forked? && can?(current_user, :remove_fork_project, @project)
%hr %hr
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
Remove fork relationship Remove fork relationship
%p.append-bottom-0 %p.append-bottom-0
...@@ -299,7 +301,7 @@ ...@@ -299,7 +301,7 @@
This will remove the fork relationship to source project This will remove the fork relationship to source project
= succeed "." do = succeed "." do
= link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
.col-lg-9 .col-lg-8
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p %p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
...@@ -307,12 +309,12 @@ ...@@ -307,12 +309,12 @@
- if can?(current_user, :remove_project, @project) - if can?(current_user, :remove_project, @project)
%hr %hr
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
Remove project Remove project
%p.append-bottom-0 %p.append-bottom-0
Removing the project will delete its repository and all related resources including issues, merge requests etc. Removing the project will delete its repository and all related resources including issues, merge requests etc.
.col-lg-9 .col-lg-8
= form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do = form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do
%p %p
%strong Removed projects cannot be restored! %strong Removed projects cannot be restored!
......
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project. used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default .col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Add webhook', class: 'btn btn-create' = f.submit 'Add webhook', class: 'btn btn-create'
......
...@@ -8,13 +8,6 @@ ...@@ -8,13 +8,6 @@
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable') = webpack_bundle_tag('issuable')
- if defined?(@issue) && @issue.confidential?
.confidential-issue-warning{ data: { spy: 'affix' } }
%span.confidential-issue-text
#{confidential_icon(@issue)} This issue is confidential.
%a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' }
What are confidential issues?
.clearfix.detail-page-header .clearfix.detail-page-header
.issuable-header .issuable-header
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
...@@ -29,6 +22,7 @@ ...@@ -29,6 +22,7 @@
= icon('angle-double-left') = icon('angle-double-left')
.issuable-meta .issuable-meta
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue") = issuable_meta(@issue, @project, "Issue")
.issuable-actions .issuable-actions
......
.dropdown.more-actions - is_current_user = current_user == note.author
- if note_editable || !is_current_user
.dropdown.more-actions
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
= icon('ellipsis-v', class: 'icon') = icon('ellipsis-v', class: 'icon')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- if note_editable
%li %li
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
%li.divider %li.divider
- unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
Report as abuse Report as abuse
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
.form-group .form-group
.col-md-9 .col-md-9
= f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light'
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
.form-group .form-group
.col-md-9 .col-md-9
= f.label :ref, _('Target Branch'), class: 'label-light' = f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
.form-group .form-group
.col-md-9 .col-md-9
......
%div{ class: badge.title.gsub(' ', '-') } %div{ class: badge.title.gsub(' ', '-') }
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= badge.title.capitalize = badge.title.capitalize
.col-lg-9 .col-lg-8
.prepend-top-10 .prepend-top-10
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
......
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Pipelines Pipelines
.col-lg-9 .col-lg-8
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature %fieldset.builds-feature
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
......
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.settings-sidebar .col-lg-4.settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Project members Project members
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%i Masters %i Masters
or or
%i Owners %i Owners
.col-lg-9 .col-lg-8
.light .light
- if can?(current_user, :admin_project_member, @project) && !membership_locked? - if can?(current_user, :admin_project_member, @project) && !membership_locked?
%ul.nav-links.project-member-tabs{ role: 'tablist' } %ul.nav-links.project-member-tabs{ role: 'tablist' }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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