Commit 4f7c11b4 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'nt/ce-to-ee-thursday' into 'master'

CE upstream: Thursday

Closes #2251 and gitaly#157

See merge request !1818
parents bc3ce22b 5a9c3672
...@@ -274,7 +274,10 @@ static-analysis: ...@@ -274,7 +274,10 @@ static-analysis:
script: script:
- scripts/static-analysis - scripts/static-analysis
docs:check:links: # Documentation checks:
# - Check validity of relative links
# - Make sure cURL examples in API docs use the full switches
docs lint:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -301,22 +304,22 @@ downtime_check: ...@@ -301,22 +304,22 @@ downtime_check:
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
rake pg db:migrate:reset: rake pg db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:migrate:reset: rake mysql db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback .db-rollback: &db-rollback
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:rollback STEP=120 - bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate - bundle exec rake db:migrate
...@@ -324,16 +327,15 @@ rake mysql db:migrate:reset: ...@@ -324,16 +327,15 @@ rake mysql db:migrate:reset:
rake pg db:rollback: rake pg db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:rollback: rake mysql db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
...@@ -351,12 +353,10 @@ rake mysql db:rollback: ...@@ -351,12 +353,10 @@ rake mysql db:rollback:
rake pg db:seed_fu: rake pg db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-pg <<: *use-pg
<<: *except-docs
rake mysql db:seed_fu: rake mysql db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
<<: *except-docs
rake gitlab:assets:compile: rake gitlab:assets:compile:
stage: test stage: test
...@@ -397,21 +397,6 @@ rake karma: ...@@ -397,21 +397,6 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -502,22 +487,6 @@ trigger_docs: ...@@ -502,22 +487,6 @@ trigger_docs:
only: only:
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
stage: post-test
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
pages: pages:
before_script: [] before_script: []
stage: pages stage: pages
...@@ -556,4 +525,4 @@ cache gems: ...@@ -556,4 +525,4 @@ cache gems:
- vendor/cache - vendor/cache
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
\ No newline at end of file
...@@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) { ...@@ -62,6 +62,7 @@ function glEmojiTag(inputName, options) {
data-fallback-src="${fallbackImageSrc}" data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute} ${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}" data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
> >
${contents} ${contents}
</gl-emoji> </gl-emoji>
......
...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
computed: { computed: {
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
} }
}, },
watch: { watch: {
......
...@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', { ...@@ -46,6 +46,7 @@ export default Vue.component('pipelines-table', {
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
updateGraphDropdown: false,
}; };
}, },
...@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', { ...@@ -130,15 +131,21 @@ export default Vue.component('pipelines-table', {
const pipelines = response.pipelines || response; const pipelines = response.pipelines || response;
this.store.storePipelines(pipelines); this.store.storePipelines(pipelines);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
...@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', { ...@@ -163,7 +170,9 @@ export default Vue.component('pipelines-table', {
v-if="shouldRenderTable"> v-if="shouldRenderTable">
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service" /> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div> </div>
</div> </div>
`, `,
......
...@@ -5,7 +5,7 @@ require('./preview_markdown'); ...@@ -5,7 +5,7 @@ require('./preview_markdown');
window.DropzoneInput = (function() { window.DropzoneInput = (function() {
function DropzoneInput(form) { function DropzoneInput(form) {
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() { ...@@ -16,7 +16,7 @@ window.DropzoneInput = (function() {
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"; btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
project_uploads_path = window.project_uploads_path || null; uploads_path = window.uploads_path || null;
max_file_size = gon.max_file_size || 10; max_file_size = gon.max_file_size || 10;
form_textarea = $(form).find(".js-gfm-input"); form_textarea = $(form).find(".js-gfm-input");
form_textarea.wrap("<div class=\"div-dropzone\"></div>"); form_textarea.wrap("<div class=\"div-dropzone\"></div>");
...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() { ...@@ -39,10 +39,10 @@ window.DropzoneInput = (function() {
"display": "none" "display": "none"
}); });
if (!project_uploads_path) return; if (!uploads_path) return;
dropzone = form_dropzone.dropzone({ dropzone = form_dropzone.dropzone({
url: project_uploads_path, url: uploads_path,
dictDefaultMessage: "", dictDefaultMessage: "",
clickable: true, clickable: true,
paramName: "file", paramName: "file",
...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() { ...@@ -159,7 +159,7 @@ window.DropzoneInput = (function() {
formData = new FormData(); formData = new FormData();
formData.append("file", item, filename); formData.append("file", item, filename);
return $.ajax({ return $.ajax({
url: project_uploads_path, url: uploads_path,
type: "POST", type: "POST",
data: formData, data: formData,
dataType: "json", dataType: "json",
......
<script> <script>
/* eslint-disable no-new */
/* global Flash */ /* global Flash */
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue'; import EnvironmentTable from './environments_table.vue';
...@@ -82,11 +80,13 @@ export default { ...@@ -82,11 +80,13 @@ export default {
eventHub.$on('refreshEnvironments', this.fetchEnvironments); eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder); eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
}, },
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshEnvironments'); eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder'); eventHub.$off('toggleFolder');
eventHub.$off('postAction');
}, },
methods: { methods: {
...@@ -144,6 +144,7 @@ export default { ...@@ -144,6 +144,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
...@@ -159,9 +160,16 @@ export default { ...@@ -159,9 +160,16 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoadingFolderContent = false; this.isLoadingFolderContent = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
},
}, },
}; };
</script> </script>
......
<script> <script>
/* global Flash */
/* eslint-disable no-new */
import playIconSvg from 'icons/_icon_play.svg'; import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -12,11 +9,6 @@ export default { ...@@ -12,11 +9,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
service: {
type: Object,
required: true,
},
}, },
data() { data() {
...@@ -37,16 +29,7 @@ export default { ...@@ -37,16 +29,7 @@ export default {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy'); $(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
isActionDisabled(action) { isActionDisabled(action) {
......
...@@ -47,11 +47,6 @@ export default { ...@@ -47,11 +47,6 @@ export default {
required: false, required: false,
}, },
service: {
type: Object,
required: true,
default: () => ({}),
},
}, },
computed: { computed: {
...@@ -560,31 +555,34 @@ export default { ...@@ -560,31 +555,34 @@ export default {
<actions-component <actions-component
v-if="hasManualActions && canCreateDeployment" v-if="hasManualActions && canCreateDeployment"
:service="service" :actions="manualActions"
:actions="manualActions"/> />
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/> :external-url="externalURL"
/>
<monitoring-button-component <monitoring-button-component
v-if="monitoringUrl && canReadEnvironment" v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/> :monitoring-url="monitoringUrl"
/>
<terminal-button-component <terminal-button-component
v-if="model && model.terminal_path" v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/> :terminal-path="model.terminal_path"
/>
<stop-component <stop-component
v-if="hasStopAction && canCreateDeployment" v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path" :stop-url="model.stop_path"
:service="service"/> />
<rollback-component <rollback-component
v-if="canRetry && canCreateDeployment" v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry-url="retryUrl" :retry-url="retryUrl"
:service="service"/> />
</div> </div>
</td> </td>
</tr> </tr>
......
<script> <script>
/* global Flash */
/* eslint-disable no-new */
/** /**
* Renders Rollback or Re deploy button in environments table depending * Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`. * of the provided property `isLastDeployment`.
...@@ -20,12 +18,6 @@ export default { ...@@ -20,12 +18,6 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
service: {
type: Object,
required: true,
default: () => ({}),
},
}, },
data() { data() {
...@@ -38,17 +30,7 @@ export default { ...@@ -38,17 +30,7 @@ export default {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); eventHub.$emit('postAction', this.retryUrl);
this.service.postAction(this.retryUrl)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
}, },
}, },
}; };
......
<script> <script>
/* global Flash */
/* eslint-disable no-new, no-alert */
/** /**
* Renders the stop "button" that allows stop an environment. * Renders the stop "button" that allows stop an environment.
* Used in environments table. * Used in environments table.
...@@ -13,12 +11,6 @@ export default { ...@@ -13,12 +11,6 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
service: {
type: Object,
required: true,
default: () => ({}),
},
}, },
data() { data() {
...@@ -35,20 +27,13 @@ export default { ...@@ -35,20 +27,13 @@ export default {
methods: { methods: {
onClick() { onClick() {
// eslint-disable-next-line no-alert
if (confirm('Are you sure you want to stop this environment?')) { if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true; this.isLoading = true;
$(this.$el).tooltip('destroy'); $(this.$el).tooltip('destroy');
this.service.postAction(this.retryUrl) eventHub.$emit('postAction', this.stopUrl);
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshEnvironments');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.', 'alert');
});
} }
}, },
}, },
......
...@@ -30,11 +30,6 @@ export default { ...@@ -30,11 +30,6 @@ export default {
default: false, default: false,
}, },
service: {
type: Object,
required: true,
},
isLoadingFolderContent: { isLoadingFolderContent: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -92,7 +87,6 @@ export default { ...@@ -92,7 +87,6 @@ export default {
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:service="service"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
/> />
...@@ -124,7 +118,8 @@ export default { ...@@ -124,7 +118,8 @@ export default {
:model="children" :model="children"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:service="service" /> />
<tr> <tr>
<td <td
......
<script> <script>
/* eslint-disable no-new */
/* global Flash */ /* global Flash */
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentTable from '../components/environments_table.vue';
...@@ -100,6 +99,7 @@ export default { ...@@ -100,6 +99,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.', 'alert'); new Flash('An error occurred while fetching the environments.', 'alert');
}); });
}, },
...@@ -183,7 +183,8 @@ export default { ...@@ -183,7 +183,8 @@ export default {
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
:service="service"/> :service="service"
/>
<table-pagination <table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
...@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() { ...@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form); new DropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
// form and textarea event listeners
this.addEventListeners();
} }
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form); gl.text.init(this.form);
// hide discard button // hide discard button
this.form.find('.js-note-discard').hide(); this.form.find('.js-note-discard').hide();
......
...@@ -19,12 +19,10 @@ ...@@ -19,12 +19,10 @@
}); });
}; };
Milestone.sortIssues = function(data) { Milestone.sortIssues = function(url, data) {
var sort_issues_url;
sort_issues_url = location.href + "/sort_issues";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_issues_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -36,12 +34,10 @@ ...@@ -36,12 +34,10 @@
}); });
}; };
Milestone.sortMergeRequests = function(data) { Milestone.sortMergeRequests = function(url, data) {
var sort_mr_url;
sort_mr_url = location.href + "/sort_merge_requests";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_mr_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -81,42 +77,55 @@ ...@@ -81,42 +77,55 @@
}; };
function Milestone() { function Milestone() {
var oldMouseStart; this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
this.bindIssuesSorting(); this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching(); this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
} }
Milestone.prototype.bindIssuesSorting = function() { Milestone.prototype.bindIssuesSorting = function() {
if (!this.issuesSortEndpoint) return;
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'issue-list', group: 'issue-list',
listEls: $('.issues-sortable-list'), listEls: $('.issues-sortable-list'),
fieldName: 'issue', fieldName: 'issue',
sortCallback: Milestone.sortIssues, sortCallback: (data) => {
Milestone.sortIssues(this.issuesSortEndpoint, data);
},
updateCallback: Milestone.updateIssue, updateCallback: Milestone.updateIssue,
}); });
}.bind(this)); }.bind(this));
}; };
Milestone.prototype.bindTabsSwitching = function() { Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
var currentTabClass, previousTabClass; const $target = $(e.target);
currentTabClass = $(e.target).data('show');
previousTabClass = $(e.relatedTarget).data('show'); location.hash = $target.attr('href');
$(previousTabClass).hide(); this.loadTab($target);
$(currentTabClass).removeClass('hidden');
return $(currentTabClass).show();
}); });
}; };
Milestone.prototype.bindMergeRequestSorting = function() { Milestone.prototype.bindMergeRequestSorting = function() {
if (!this.mergeRequestsSortEndpoint) return;
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'merge-request-list', group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request', fieldName: 'merge_request',
sortCallback: Milestone.sortMergeRequests, sortCallback: (data) => {
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
},
updateCallback: Milestone.updateMergeRequest, updateCallback: Milestone.updateMergeRequest,
}); });
}.bind(this)); }.bind(this));
...@@ -169,6 +178,35 @@ ...@@ -169,6 +178,35 @@
}); });
}; };
Milestone.prototype.loadInitialTab = function() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
if (tabElId === '#tab-merge-requests') {
this.bindMergeRequestSorting();
}
});
}
};
return Milestone; return Milestone;
})(); })();
}).call(window); }).call(window);
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
import d3 from 'd3'; import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash'; import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container'; const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state'; const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
...@@ -36,6 +40,7 @@ class PrometheusGraph { ...@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right; this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} else { } else {
...@@ -74,6 +79,12 @@ class PrometheusGraph { ...@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show(); $(prometheusParentGraphContainer).show();
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
} }
}); });
} }
...@@ -96,6 +107,7 @@ class PrometheusGraph { ...@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top) .attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g') .append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`); .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer) const axisLabelContainer = d3.select(prometheusGraphContainer)
...@@ -116,6 +128,7 @@ class PrometheusGraph { ...@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y) .scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks) .ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width) .tickSize(-this.width)
.outerTickSize(0)
.orient('left'); .orient('left');
this.createAxisLabelContainers(axisLabelContainer, key); this.createAxisLabelContainers(axisLabelContainer, key);
...@@ -248,7 +261,8 @@ class PrometheusGraph { ...@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex]; const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0; const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time); const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData); const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
...@@ -256,13 +270,12 @@ class PrometheusGraph { ...@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag // Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line') currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({ .attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate, x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0), y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate, x2: currentTimeCoordinate,
...@@ -272,33 +285,45 @@ class PrometheusGraph { ...@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle') currentChart.append('circle')
.attr('class', 'circle-metric') .attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color) .attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate) .attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value)) .attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric); .attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text // The little box with text
const rectTextMetric = currentChart.append('g') const rectTextMetric = currentChart.append('svg')
.attr('class', 'rect-text-metric') .attr({
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect') rectTextMetric.append('rect')
.attr('class', 'rect-metric') .attr({
.attr('x', currentTimeCoordinate + 10) class: 'rect-metric',
.attr('y', maxMetricValue) x: 4,
.attr('width', this.commonGraphProperties.rect_text_width) y: 1,
.attr('height', this.commonGraphProperties.rect_text_height); rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric') .attr({
.attr('x', currentTimeCoordinate + 35) class: 'text-metric text-metric-bold',
.attr('y', maxMetricValue + 35) x: 8,
y: 35,
})
.text(timeFormat(currentData.time)); .text(timeFormat(currentData.time));
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric-date') .attr({
.attr('x', currentTimeCoordinate + 15) class: 'text-metric-date',
.attr('y', maxMetricValue + 15) x: 8,
.text(dayFormat(currentData.time)); y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value); let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') { if (key === 'cpu_values') {
......
This diff is collapsed.
<script>
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
/* global Flash */ /* global Flash */
import StatusIconEntityMap from '../../ci_status_icons'; import StatusIconEntityMap from '../../ci_status_icons';
...@@ -7,36 +22,55 @@ export default { ...@@ -7,36 +22,55 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
builds: '', isLoading: false,
spinner: '<span class="fa fa-spinner fa-spin"></span>', dropdownContent: '',
endpoint: this.stage.dropdown_path,
}; };
}, },
updated() { updated() {
if (this.builds) { if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
} }
}, },
methods: { watch: {
fetchBuilds(e) { updateDropdown() {
const ariaExpanded = e.currentTarget.attributes['aria-expanded']; if (this.updateDropdown &&
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs();
}
},
},
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null; methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
this.isLoading = true;
this.fetchJobs();
}
},
return this.$http.get(this.stage.dropdown_path) fetchJobs() {
this.$http.get(this.endpoint)
.then((response) => { .then((response) => {
this.builds = JSON.parse(response.body).html; this.dropdownContent = response.json().html;
this.isLoading = false;
}) })
.catch(() => { .catch(() => {
// If dropdown is opened we'll close it. this.closeDropdown();
if (this.$el.classList.contains('open')) { this.isLoading = false;
$(this.$refs.dropdown).dropdown('toggle');
}
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
return flash; return flash;
...@@ -57,59 +91,83 @@ export default { ...@@ -57,59 +91,83 @@ export default {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
}, },
computed: { computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() { dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container'; return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
}, },
triggerButtonClass() { triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; return `ci-status-icon-${this.stage.status.group}`;
}, },
svgHTML() {
svgIcon() {
return StatusIconEntityMap[this.stage.status.icon]; return StatusIconEntityMap[this.stage.status.icon];
}, },
}, },
template: ` };
<div> </script>
<button
@click="fetchBuilds($event)" <template>
:class="triggerButtonClass" <div class="dropdown">
:title="stage.title" <button
data-placement="top" :class="triggerButtonClass"
data-toggle="dropdown" @click="onClickStage"
type="button" class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button"
:aria-label="stage.title" :title="stage.title"
ref="dropdown"> data-placement="top"
<span data-toggle="dropdown"
v-html="svgHTML" type="button"
aria-hidden="true"> id="stageDropdown"
</span> aria-haspopup="true"
<i aria-expanded="false">
class="fa fa-caret-down"
aria-hidden="true" /> <span
</button> v-html="svgIcon"
<ul aria-hidden="true"
ref="dropdown-content" :aria-label="stage.title">
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> </span>
<div
class="arrow-up" <i
aria-hidden="true"></div> class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown">
<li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu">
<div <div
:class="dropdownClass" class="text-center"
class="js-builds-dropdown-list scrollable-menu" v-if="isLoading">
v-html="buildsOrSpinner"> <i
class="fa fa-spin fa-spinner"
aria-hidden="true"
aria-label="Loading">
</i>
</div> </div>
</ul>
</div> <ul
`, v-else
}; v-html="dropdownContent">
</ul>
</li>
</ul>
</div>
</script>
...@@ -49,6 +49,7 @@ export default { ...@@ -49,6 +49,7 @@ export default {
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false, isMakingRequest: false,
updateGraphDropdown: false,
}; };
}, },
...@@ -198,15 +199,21 @@ export default { ...@@ -198,15 +199,21 @@ export default {
this.store.storePagination(response.headers); this.store.storePagination(response.headers);
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = true;
}, },
errorCallback() { errorCallback() {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
if (isMakingRequest) {
this.updateGraphDropdown = false;
}
}, },
}, },
...@@ -263,7 +270,9 @@ export default { ...@@ -263,7 +270,9 @@ export default {
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:service="service"/> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</div> </div>
<gl-pagination <gl-pagination
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
$els.each((function(_this) { $els.each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id'); options.groupId = $dropdown.data('group-id');
...@@ -38,11 +38,11 @@ ...@@ -38,11 +38,11 @@
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
selectedId = $dropdown.data('selected');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
issueURL = $dropdown.data('issueUpdate'); issueURL = $dropdown.data('issueUpdate');
$selectbox = $dropdown.closest('.selectbox'); $selectbox = $dropdown.closest('.selectbox');
...@@ -51,6 +51,8 @@ ...@@ -51,6 +51,8 @@
$value = $block.find('.value'); $value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault;
var updateIssueBoardsIssue = function () { var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
...@@ -186,12 +188,14 @@ ...@@ -186,12 +188,14 @@
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
$dropdown.find('.dropdown-toggle-text').removeClass('is-default');
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
return selected.name; return selected.name;
} }
} else { } else {
$dropdown.find('.dropdown-toggle-text').addClass('is-default');
return defaultLabel; return defaultLabel;
} }
}, },
...@@ -204,13 +208,14 @@ ...@@ -204,13 +208,14 @@
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) { clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected, isSelecting;
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault(); e.preventDefault();
selectedId = user.id;
if (selectedId === gon.current_user_id) { if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide(); $('.assign-to-me-link').hide();
} else { } else {
...@@ -221,12 +226,11 @@ ...@@ -221,12 +226,11 @@
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) { if (user.id && isSelecting) {
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id, id: user.id,
username: user.username, username: user.username,
...@@ -248,6 +252,9 @@ ...@@ -248,6 +252,9 @@
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
}
$el.find('.is-active').removeClass('is-active'); $el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
}, },
......
...@@ -10,13 +10,18 @@ export default { ...@@ -10,13 +10,18 @@ export default {
pipelines: { pipelines: {
type: Array, type: Array,
required: true, required: true,
default: () => ([]),
}, },
service: { service: {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
components: { components: {
...@@ -40,7 +45,9 @@ export default { ...@@ -40,7 +45,9 @@ export default {
v-bind:model="model"> v-bind:model="model">
<tr is="pipelines-table-row-component" <tr is="pipelines-table-row-component"
:pipeline="model" :pipeline="model"
:service="service"></tr> :service="service"
:update-graph-dropdown="updateGraphDropdown"
/>
</template> </template>
</tbody> </tbody>
</table> </table>
......
...@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; ...@@ -3,7 +3,7 @@ import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status'; import PipelinesStatusComponent from '../../pipelines/components/status';
import PipelinesStageComponent from '../../pipelines/components/stage'; import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit'; import CommitComponent from './commit';
...@@ -24,6 +24,12 @@ export default { ...@@ -24,6 +24,12 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
components: { components: {
...@@ -213,7 +219,10 @@ export default { ...@@ -213,7 +219,10 @@ export default {
<div class="stage-container dropdown js-mini-pipeline-graph" <div class="stage-container dropdown js-mini-pipeline-graph"
v-if="pipeline.details.stages.length > 0" v-if="pipeline.details.stages.length > 0"
v-for="stage in pipeline.details.stages"> v-for="stage in pipeline.details.stages">
<dropdown-stage :stage="stage"/>
<dropdown-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"/>
</div> </div>
</td> </td>
......
...@@ -390,7 +390,8 @@ ...@@ -390,7 +390,8 @@
&::before { &::before {
position: absolute; position: absolute;
left: 6px; left: 6px;
top: 6px; top: 50%;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
......
...@@ -425,12 +425,6 @@ ...@@ -425,12 +425,6 @@
float: right; float: right;
} }
.diffs {
.content-block {
border-bottom: none;
}
}
.files-changed { .files-changed {
border-bottom: none; border-bottom: none;
} }
......
...@@ -307,7 +307,8 @@ ...@@ -307,7 +307,8 @@
.prometheus-graph { .prometheus-graph {
text { text {
fill: $stat-graph-axis-fill; fill: $gl-text-color;
stroke-width: 0;
} }
.label-axis-text, .label-axis-text,
...@@ -360,27 +361,33 @@ ...@@ -360,27 +361,33 @@
.rect-text-metric { .rect-text-metric {
fill: $white-light; fill: $white-light;
stroke-width: 1; stroke-width: 1;
stroke: $black; stroke: $gray-darkest;
} }
.rect-axis-text { .rect-axis-text {
fill: $white-light; fill: $white-light;
} }
.text-metric, .text-metric {
.text-median-metric, font-weight: 600;
.text-metric-usage,
.text-metric-date {
fill: $black;
} }
.text-metric-date { .selected-metric-line {
font-weight: 200; stroke: $gl-gray-dark;
stroke-width: 1;
} }
.selected-metric-line { .deployment-line {
stroke: $black; stroke: $black;
stroke-width: 1; stroke-width: 2;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
} }
.prometheus-state { .prometheus-state {
......
...@@ -6,7 +6,13 @@ ...@@ -6,7 +6,13 @@
} }
.limit-container-width { .limit-container-width {
.detail-page-header { .detail-page-header,
.page-content-header,
.commit-box,
.info-well,
.notes,
.commit-ci-menu,
.files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
...@@ -36,8 +42,7 @@ ...@@ -36,8 +42,7 @@
} }
.diffs { .diffs {
.mr-version-controls, .mr-version-controls {
.files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
} }
......
...@@ -183,3 +183,55 @@ ...@@ -183,3 +183,55 @@
width: auto; width: auto;
} }
} }
.flex-project-members-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: $screen-sm-min) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-project-members-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.panel {
.panel-heading {
.badge {
margin-top: 0;
}
@media (max-width: $screen-sm-min) {
.badge {
margin-right: 0;
margin-left: 0;
}
}
}
}
...@@ -511,7 +511,6 @@ ...@@ -511,7 +511,6 @@
.mr-version-controls { .mr-version-controls {
background: $gray-light; background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color; color: $gl-text-color;
.mr-version-menus-container { .mr-version-menus-container {
......
...@@ -67,7 +67,7 @@ ul.notes { ...@@ -67,7 +67,7 @@ ul.notes {
} }
} }
&.is-editting { &.is-editing {
.note-header, .note-header,
.note-text, .note-text,
.edited-text { .edited-text {
......
...@@ -785,16 +785,11 @@ ...@@ -785,16 +785,11 @@
} }
.scrollable-menu { .scrollable-menu {
padding: 0;
max-height: 245px; max-height: 245px;
overflow: auto; overflow: auto;
} }
// Loading icon
.builds-dropdown-loading {
margin: 0 auto;
width: 20px;
}
// Action icon on the right // Action icon on the right
a.ci-action-icon-wrapper { a.ci-action-icon-wrapper {
color: $action-icon-color; color: $action-icon-color;
...@@ -897,30 +892,29 @@ ...@@ -897,30 +892,29 @@
* Top arrow in the dropdown in the mini pipeline graph * Top arrow in the dropdown in the mini pipeline graph
*/ */
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
.arrow-up {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::before { &::before,
border-width: 0 5px 5px; &::after {
border-bottom-color: $border-color; content: '';
} display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::after { &::before {
margin-top: 1px; border-width: 0 5px 5px;
border-bottom-color: $white-light; border-bottom-color: $border-color;
} }
&::after {
margin-top: 1px;
border-bottom-color: $white-light;
} }
} }
......
...@@ -71,7 +71,6 @@ ...@@ -71,7 +71,6 @@
.nav-controls { .nav-controls {
width: auto; width: auto;
min-width: 50%; min-width: 50%;
white-space: nowrap;
} }
} }
......
module MilestoneActions
extend ActiveSupport::Concern
def merge_requests
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.merge_requests,
show_project_name: true
})
end
end
end
def participants
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
users: @milestone.participants
})
end
end
end
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
labels: @milestone.labels
})
end
end
end
private
def tabs_json(partial, data = {})
{
html: view_to_html_string(partial, data)
}
end
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
end
end
end
...@@ -5,10 +5,12 @@ module SnippetsActions ...@@ -5,10 +5,12 @@ module SnippetsActions
end end
def raw def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
send_data( send_data(
convert_line_endings(@snippet.content), convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8', type: 'text/plain; charset=utf-8',
disposition: 'inline', disposition: disposition,
filename: @snippet.sanitized_file_name filename: @snippet.sanitized_file_name
) )
end end
......
module UploadsActions
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 unless uploader.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
end
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:show, :update] before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index def index
......
...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController ...@@ -89,4 +89,8 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user) return render_404 unless @project.feature_available?(:builds, current_user)
end end
def require_pages_enabled!
not_found unless Gitlab.config.pages.enabled
end
end end
...@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -16,7 +16,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def browse def browse
directory = params[:path] ? "#{params[:path]}/" : '' @path = params[:path]
directory = @path ? "#{@path}/" : ''
@entry = build.artifacts_metadata_entry(directory) @entry = build.artifacts_metadata_entry(directory)
render_404 unless @entry.exists? render_404 unless @entry.exists?
...@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -60,7 +61,10 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def build def build
@build ||= build_from_id || build_from_ref @build ||= begin
build = build_from_id || build_from_ref
build&.present(current_user: current_user)
end
end end
def build_from_id def build_from_id
......
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
.represent_concise(deployments) }
end
private
def environment
@environment ||= project.environments.find(params[:environment_id])
end
end
...@@ -123,7 +123,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -123,7 +123,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
define_diff_comment_vars define_diff_comment_vars
else else
build_merge_request build_merge_request
@diffs = @merge_request.diffs(diff_options) @compare = @merge_request
@diffs = @compare.diffs(diff_options)
@diff_notes_disabled = true @diff_notes_disabled = true
end end
...@@ -645,12 +646,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -645,12 +646,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
@diffs = @compare =
if @start_sha if @start_sha
@merge_request_diff.compare_with(@start_sha).diffs(diff_options) @merge_request_diff.compare_with(@start_sha)
else else
@merge_request_diff.diffs(diff_options) @merge_request_diff
end end
@diffs = @compare.diffs(diff_options)
end end
def define_diff_comment_vars def define_diff_comment_vars
...@@ -659,11 +662,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -659,11 +662,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
noteable_id: @merge_request.id noteable_id: @merge_request.id
} }
@diff_notes_disabled = !@merge_request_diff.latest? || @start_sha @diff_notes_disabled = false
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@merge_request_diff.diff_refs) @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end end
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :module_enabled before_action :module_enabled
before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels]
# Allow read any milestone # Allow read any milestone
before_action :authorize_read_milestone! before_action :authorize_read_milestone!
# Allow admin milestone # Allow admin milestone
before_action :authorize_admin_milestone!, except: [:index, :show] before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
respond_to :html respond_to :html
......
class Projects::PagesController < Projects::ApplicationController class Projects::PagesController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_read_pages!, only: [:show] before_action :authorize_read_pages!, only: [:show]
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
......
class Projects::PagesDomainsController < Projects::ApplicationController class Projects::PagesDomainsController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show] before_action :authorize_update_pages!, except: [:show]
before_action :domain, only: [:show, :destroy] before_action :domain, only: [:show, :destroy]
......
...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
.new(project) .new(project, scope: @scope)
.execute(scope: @scope) .execute
.page(params[:page]) .page(params[:page])
.per(30) .per(30)
@running_count = PipelinesFinder @running_count = PipelinesFinder
.new(project).execute(scope: 'running').count .new(project, scope: 'running').execute.count
@pending_count = PipelinesFinder @pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count .new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder @finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count .new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder @pipelines_count = PipelinesFinder
.new(project).execute.count .new(project).execute.count
......
...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::RawController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
if @blob.valid_lfs_pointer? if @blob.stored_externally?
send_lfs_object send_lfs_object
else else
send_git_blob @repository, @blob send_git_blob @repository, @blob
......
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create]
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
execute
respond_to do |format|
if link_to_file
format.json do
render json: { link: link_to_file }
end
else
format.json do
render json: 'Invalid file.', status: :unprocessable_entity
end
end
end
end
def show
return render_404 if uploader.nil? || !uploader.file.exists?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
end
private private
def uploader def uploader
...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -52,4 +30,10 @@ class Projects::UploadsController < Projects::ApplicationController
def image_or_video? def image_or_video?
uploader && uploader.file.exists? && uploader.image_or_video? uploader && uploader.file.exists? && uploader.image_or_video?
end end
def uploader_class
FileUploader
end
alias_method :model, :project
end end
...@@ -5,10 +5,10 @@ class SnippetsController < ApplicationController ...@@ -5,10 +5,10 @@ class SnippetsController < ApplicationController
include SnippetsActions include SnippetsActions
include RendersBlob include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet # Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw, :download] before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -16,7 +16,7 @@ class SnippetsController < ApplicationController ...@@ -16,7 +16,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet # Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy] before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets' layout 'snippets'
respond_to :html respond_to :html
...@@ -88,14 +88,6 @@ class SnippetsController < ApplicationController ...@@ -88,14 +88,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path redirect_to snippets_path
end end
def download
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
def preview_markdown def preview_markdown
result = PreviewMarkdownService.new(@project, current_user, params).execute result = PreviewMarkdownService.new(@project, current_user, params).execute
......
class UploadsController < ApplicationController class UploadsController < ApplicationController
skip_before_action :authenticate_user! include UploadsActions
before_action :find_model, :authorize_access!
def show
uploader = @model.send(upload_mount)
unless uploader.file_storage?
return redirect_to uploader.url
end
unless uploader.file && uploader.file.exists? skip_before_action :authenticate_user!
return render_404 before_action :find_model
end before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
private private
def find_model def find_model
unless upload_model && upload_mount return render_404 unless upload_model && upload_mount
return render_404
end
@model = upload_model.find(params[:id]) @model = upload_model.find(params[:id])
end end
def authorize_access! def authorize_access!
authorized = authorized =
case @model case model
when Project
can?(current_user, :read_project, @model)
when Group
can?(current_user, :read_group, @model)
when Note when Note
can?(current_user, :read_project, @model.project) can?(current_user, :read_project, model.project)
else when User
# No authentication required for user avatars.
true true
else
permission = "read_#{model.class.to_s.underscore}".to_sym
can?(current_user, permission, model)
end end
return if authorized render_unauthorized unless authorized
end
def authorize_create_access!
# for now we support only personal snippets comments
authorized = can?(current_user, :comment_personal_snippet, model)
render_unauthorized unless authorized
end
def render_unauthorized
if current_user if current_user
render_404 render_404
else else
...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController ...@@ -58,17 +51,44 @@ class UploadsController < ApplicationController
"project" => Project, "project" => Project,
"note" => Note, "note" => Note,
"group" => Group, "group" => Group,
"appearance" => Appearance "appearance" => Appearance,
"personal_snippet" => PersonalSnippet
} }
upload_models[params[:model]] upload_models[params[:model]]
end end
def upload_mount def upload_mount
return true unless params[:mounted_as]
upload_mounts = %w(avatar attachment file logo header_logo) upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as]) if upload_mounts.include?(params[:mounted_as])
params[:mounted_as] params[:mounted_as]
end end
end end
def uploader
return @uploader if defined?(@uploader)
if model.is_a?(PersonalSnippet)
@uploader = PersonalFileUploader.new(model, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.send(upload_mount)
redirect_to @uploader.url unless @uploader.file_storage?
end
@uploader
end
def uploader_class
PersonalFileUploader
end
def model
@model ||= find_model
end
end end
class PipelinesFinder class PipelinesFinder
attr_reader :project, :pipelines attr_reader :project, :pipelines, :params
def initialize(project) ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
def initialize(project, params = {})
@project = project @project = project
@pipelines = project.pipelines @pipelines = project.pipelines
@params = params
end end
def execute(scope: nil) def execute
scoped_pipelines = items = pipelines
case scope items = by_scope(items)
when 'running' items = by_status(items)
pipelines.running items = by_ref(items)
when 'pending' items = by_name(items)
pipelines.pending items = by_username(items)
when 'finished' items = by_yaml_errors(items)
pipelines.finished sort_items(items)
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
pipelines
end
scoped_pipelines.order(id: :desc)
end end
private private
...@@ -43,4 +37,78 @@ class PipelinesFinder ...@@ -43,4 +37,78 @@ class PipelinesFinder
def tags def tags
project.repository.tag_names project.repository.tag_names
end end
def by_scope(items)
case params[:scope]
when 'running'
items.running
when 'pending'
items.pending
when 'finished'
items.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
items
end
end
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
def by_ref(items)
if params[:ref].present?
items.where(ref: params[:ref])
else
items
end
end
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
else
items
end
end
def by_username(items)
if params[:username].present?
items.joins(:user).where(users: { username: params[:username] })
else
items
end
end
def by_yaml_errors(items)
case Gitlab::Utils.to_boolean(params[:yaml_errors])
when true
items.where("yaml_errors IS NOT NULL")
when false
items.where("yaml_errors IS NULL")
else
items
end
end
def sort_items(items)
order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
params[:order_by]
else
:id
end
sort = if params[:sort] =~ /\A(ASC|DESC)\z/i
params[:sort]
else
:desc
end
items.order(order_by => sort)
end
end end
...@@ -52,7 +52,7 @@ module BlobHelper ...@@ -52,7 +52,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.valid_lfs_pointer? elsif blob.stored_externally?
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
...@@ -95,7 +95,7 @@ module BlobHelper ...@@ -95,7 +95,7 @@ module BlobHelper
end end
def can_modify_blob?(blob, project = @project, ref = @ref) def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.valid_lfs_pointer? && can_edit_tree?(project, ref) !blob.stored_externally? && can_edit_tree?(project, ref)
end end
def leave_edit_message def leave_edit_message
...@@ -223,7 +223,9 @@ module BlobHelper ...@@ -223,7 +223,9 @@ module BlobHelper
end end
def open_raw_blob_button(blob) def open_raw_blob_button(blob)
if blob.raw_binary? return if blob.empty?
if blob.raw_binary? || blob.stored_externally?
icon = icon('download') icon = icon('download')
title = 'Download' title = 'Download'
else else
...@@ -244,19 +246,27 @@ module BlobHelper ...@@ -244,19 +246,27 @@ module BlobHelper
viewer.max_size viewer.max_size
end end
"it is larger than #{number_to_human_size(max_size)}" "it is larger than #{number_to_human_size(max_size)}"
when :server_side_but_stored_in_lfs when :server_side_but_stored_externally
"it is stored in LFS" case viewer.blob.external_storage
when :lfs
'it is stored in LFS'
else
'it is stored externally'
end
end end
end end
def blob_render_error_options(viewer) def blob_render_error_options(viewer)
error = viewer.render_error
options = [] options = []
if viewer.render_error == :too_large && viewer.can_override_max_size? if error == :too_large && viewer.can_override_max_size?
options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil))) options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
end end
if viewer.rich? && viewer.blob.rendered_as_text? # If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
# so don't bother switching.
if viewer.rich? && viewer.blob.rendered_as_text? && error != :server_side_but_stored_externally
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' }) options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end end
......
...@@ -132,4 +132,28 @@ module MilestonesHelper ...@@ -132,4 +132,28 @@ module MilestonesHelper
content_tag(:div, message.html_safe, id: "data-warning", class: "settings-message prepend-top-20") content_tag(:div, message.html_safe, id: "data-warning", class: "settings-message prepend-top-20")
end end
end end
def milestone_merge_request_tab_path(milestone)
if @project
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_participants_tab_path(milestone)
if @project
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_labels_tab_path(milestone)
if @project
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
end end
...@@ -60,20 +60,16 @@ module NotesHelper ...@@ -60,20 +60,16 @@ module NotesHelper
note.project.team.human_max_access(note.author_id) note.project.team.human_max_access(note.author_id)
end end
def discussion_diff_path(discussion) def discussion_path(discussion)
if discussion.for_merge_request? && discussion.diff_discussion? if discussion.for_merge_request?
if discussion.active? return unless discussion.diff_discussion?
# Without a diff ID, the link always points to the latest diff version
diff_id = nil version_params = discussion.merge_request_version_params
elsif merge_request_diff = discussion.latest_merge_request_diff return unless version_params
diff_id = merge_request_diff.id
else path_params = version_params.merge(anchor: discussion.line_code)
# If the discussion is not active, and we cannot find the latest
# merge request diff for this discussion, we return no path at all. diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, path_params)
return
end
diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, diff_id: diff_id, anchor: discussion.line_code)
elsif discussion.for_commit? elsif discussion.for_commit?
anchor = discussion.line_code if discussion.diff_discussion? anchor = discussion.line_code if discussion.diff_discussion?
......
...@@ -8,6 +8,14 @@ module SnippetsHelper ...@@ -8,6 +8,14 @@ module SnippetsHelper
end end
end end
def download_snippet_path(snippet)
if snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
else
raw_snippet_path(snippet, inline: false)
end
end
# Return the path of a snippets index for a user or for a project # Return the path of a snippets index for a user or for a project
# #
# @returns String, path to snippet index # @returns String, path to snippet index
......
...@@ -76,7 +76,7 @@ module TreeHelper ...@@ -76,7 +76,7 @@ module TreeHelper
"A new branch will be created in your fork and a new merge request will be started." "A new branch will be created in your fork and a new merge request will be started."
end end
def tree_breadcrumbs(tree, max_links = 2) def path_breadcrumbs(max_links = 6)
if @path.present? if @path.present?
part_path = "" part_path = ""
parts = @path.split('/') parts = @path.split('/')
...@@ -88,7 +88,7 @@ module TreeHelper ...@@ -88,7 +88,7 @@ module TreeHelper
part_path = part if part_path.empty? part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part) next if parts.count > max_links && !parts.last(2).include?(part)
yield(part, tree_join(@ref, part_path), part_path) yield(part, part_path)
end end
end end
end end
......
...@@ -75,19 +75,37 @@ class Blob < SimpleDelegator ...@@ -75,19 +75,37 @@ class Blob < SimpleDelegator
end end
def no_highlighting? def no_highlighting?
size && size > MAXIMUM_TEXT_HIGHLIGHT_SIZE raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
end
def empty?
raw_size == 0
end end
def too_large? def too_large?
size && truncated? size && truncated?
end end
def external_storage_error?
if external_storage == :lfs
!project&.lfs_enabled?
else
false
end
end
def stored_externally?
return @stored_externally if defined?(@stored_externally)
@stored_externally = external_storage && !external_storage_error?
end
# Returns the size of the file that this blob represents. If this blob is an # Returns the size of the file that this blob represents. If this blob is an
# LFS pointer, this is the size of the file stored in LFS. Otherwise, this is # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
# the size of the blob itself. # the size of the blob itself.
def raw_size def raw_size
if valid_lfs_pointer? if stored_externally?
lfs_size external_size
else else
size size
end end
...@@ -98,9 +116,13 @@ class Blob < SimpleDelegator ...@@ -98,9 +116,13 @@ class Blob < SimpleDelegator
# text-based rich blob viewer matched on the file's extension. Otherwise, this # text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself. # depends on the type of the blob itself.
def raw_binary? def raw_binary?
if valid_lfs_pointer? if stored_externally?
if rich_viewer if rich_viewer
rich_viewer.binary? rich_viewer.binary?
elsif Linguist::Language.find_by_filename(name).any?
false
elsif _mime_type
_mime_type.binary?
else else
true true
end end
...@@ -118,15 +140,7 @@ class Blob < SimpleDelegator ...@@ -118,15 +140,7 @@ class Blob < SimpleDelegator
end end
def readable_text? def readable_text?
text? && !valid_lfs_pointer? && !too_large? text? && !stored_externally? && !too_large?
end
def valid_lfs_pointer?
lfs_pointer? && project&.lfs_enabled?
end
def invalid_lfs_pointer?
lfs_pointer? && !project&.lfs_enabled?
end end
def simple_viewer def simple_viewer
...@@ -165,10 +179,10 @@ class Blob < SimpleDelegator ...@@ -165,10 +179,10 @@ class Blob < SimpleDelegator
end end
def rich_viewer_class def rich_viewer_class
return if invalid_lfs_pointer? || empty? return if empty? || external_storage_error?
classes = classes =
if valid_lfs_pointer? if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS BINARY_VIEWERS + TEXT_VIEWERS
elsif binary? elsif binary?
BINARY_VIEWERS BINARY_VIEWERS
......
...@@ -70,12 +70,13 @@ module BlobViewer ...@@ -70,12 +70,13 @@ module BlobViewer
return @render_error if defined?(@render_error) return @render_error if defined?(@render_error)
@render_error = @render_error =
if server_side_but_stored_in_lfs? if server_side_but_stored_externally?
# Files stored in LFS can only be rendered using a client-side viewer, # Files that are not stored in the repository, like LFS files and
# build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the # since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from # server side. Client-side viewers use JS and can fetch the file from
# `blob_raw_url` using AJAX. # `blob_raw_url` using AJAX.
:server_side_but_stored_in_lfs :server_side_but_stored_externally
elsif override_max_size ? absolutely_too_large? : too_large? elsif override_max_size ? absolutely_too_large? : too_large?
:too_large :too_large
end end
...@@ -89,8 +90,8 @@ module BlobViewer ...@@ -89,8 +90,8 @@ module BlobViewer
private private
def server_side_but_stored_in_lfs? def server_side_but_stored_externally?
server_side? && blob.valid_lfs_pointer? server_side? && blob.stored_externally?
end end
end end
end end
...@@ -236,8 +236,8 @@ class Commit ...@@ -236,8 +236,8 @@ class Commit
project.pipelines.where(sha: sha) project.pipelines.where(sha: sha)
end end
def latest_pipeline def last_pipeline
pipelines.last @last_pipeline ||= pipelines.last
end end
def status(ref = nil) def status(ref = nil)
......
module BlobLike
extend ActiveSupport::Concern
include Linguist::BlobHelper
def id
raise NotImplementedError
end
def name
raise NotImplementedError
end
def path
raise NotImplementedError
end
def size
0
end
def data
nil
end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def truncated?
false
end
def external_storage
nil
end
def external_size
nil
end
end
...@@ -11,6 +11,7 @@ module DiscussionOnDiff ...@@ -11,6 +11,7 @@ module DiscussionOnDiff
:diff_line, :diff_line,
:for_line?, :for_line?,
:active?, :active?,
:created_at_diff?,
to: :first_note to: :first_note
......
...@@ -30,6 +30,10 @@ module NoteOnDiff ...@@ -30,6 +30,10 @@ module NoteOnDiff
raise NotImplementedError raise NotImplementedError
end end
def created_at_diff?(diff_refs)
false
end
private private
def noteable_diff_refs def noteable_diff_refs
......
...@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion ...@@ -10,7 +10,6 @@ class DiffDiscussion < Discussion
delegate :position, delegate :position,
:original_position, :original_position,
:latest_merge_request_diff,
to: :first_note to: :first_note
...@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion ...@@ -18,6 +17,25 @@ class DiffDiscussion < Discussion
false false
end end
def merge_request_version_params
return unless for_merge_request?
if active?
{}
else
diff_refs = position.diff_refs
if diff = noteable.merge_request_diff_for(diff_refs)
{ diff_id: diff.id }
elsif diff = noteable.merge_request_diff_for(diff_refs.head_sha)
{
diff_id: diff.id,
start_sha: diff_refs.start_sha
}
end
end
end
def reply_attributes def reply_attributes
super.merge( super.merge(
original_position: original_position.to_json, original_position: original_position.to_json,
......
...@@ -70,10 +70,11 @@ class DiffNote < Note ...@@ -70,10 +70,11 @@ class DiffNote < Note
self.position.diff_refs == diff_refs self.position.diff_refs == diff_refs
end end
def latest_merge_request_diff def created_at_diff?(diff_refs)
return unless for_merge_request? return false unless supported?
return true if for_commit?
self.noteable.merge_request_diff_for(self.position.diff_refs) self.original_position.diff_refs == diff_refs
end end
private private
......
...@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion ...@@ -9,14 +9,14 @@ class LegacyDiffDiscussion < Discussion
memoized_values << :active memoized_values << :active
def legacy_diff_discussion?
true
end
def self.note_class def self.note_class
LegacyDiffNote LegacyDiffNote
end end
def legacy_diff_discussion?
true
end
def active?(*args) def active?(*args)
return @active if @active.present? return @active if @active.present?
...@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion ...@@ -27,6 +27,16 @@ class LegacyDiffDiscussion < Discussion
!active? !active?
end end
def merge_request_version_params
return unless for_merge_request?
if active?
{}
else
nil
end
end
def reply_attributes def reply_attributes
super.merge(line_code: line_code) super.merge(line_code: line_code)
end end
......
...@@ -397,12 +397,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -397,12 +397,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true) merge_request_diff(true)
end end
def merge_request_diff_for(diff_refs) def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs ||= Hash.new do |h, diff_refs| @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
h[diff_refs] = merge_request_diffs.viewable.select_without_diff.find_by_diff_refs(diff_refs) diffs = merge_request_diffs.viewable.select_without_diff
end h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
@merge_request_diffs_by_diff_refs[diff_refs] diffs.find_by_diff_refs(diff_refs_or_sha)
else
diffs.find_by(head_commit_sha: diff_refs_or_sha)
end
end
@merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
end end
def reload_diff_if_branch_changed def reload_diff_if_branch_changed
......
...@@ -117,11 +117,19 @@ class Note < ActiveRecord::Base ...@@ -117,11 +117,19 @@ class Note < ActiveRecord::Base
end end
def grouped_diff_discussions(diff_refs = nil) def grouped_diff_discussions(diff_refs = nil)
diff_notes. groups = {}
fresh.
discussions. diff_notes.fresh.discussions.each do |discussion|
select { |n| n.active?(diff_refs) }. if discussion.active?(diff_refs)
group_by(&:line_code) discussions = groups[discussion.line_code] ||= []
elsif diff_refs && discussion.created_at_diff?(diff_refs)
discussions = groups[discussion.original_line_code] ||= []
end
discussions << discussion if discussions
end
groups
end end
def count_for_collection(ids, type) def count_for_collection(ids, type)
...@@ -147,10 +155,6 @@ class Note < ActiveRecord::Base ...@@ -147,10 +155,6 @@ class Note < ActiveRecord::Base
true true
end end
def latest_merge_request_diff
nil
end
def max_attachment_size def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i current_application_settings.max_attachment_size.megabytes.to_i
end end
......
...@@ -50,5 +50,16 @@ module ChatMessage ...@@ -50,5 +50,16 @@ module ChatMessage
def link(text, url) def link(text, url)
"[#{text}](#{url})" "[#{text}](#{url})"
end end
def pretty_duration(seconds)
parse_string =
if duration < 1.hour
'%M:%S'
else
'%H:%M:%S'
end
Time.at(seconds).utc.strftime(parse_string)
end
end end
end end
...@@ -15,7 +15,7 @@ module ChatMessage ...@@ -15,7 +15,7 @@ module ChatMessage
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref] @ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status] @status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration] @duration = pipeline_attributes[:duration].to_i
@pipeline_id = pipeline_attributes[:id] @pipeline_id = pipeline_attributes[:id]
end end
...@@ -37,7 +37,7 @@ module ChatMessage ...@@ -37,7 +37,7 @@ module ChatMessage
{ {
title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}", title: "Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: "in #{duration} #{time_measure}", text: "in #{pretty_duration(duration)}",
image: user_avatar || '' image: user_avatar || ''
} }
end end
...@@ -45,7 +45,7 @@ module ChatMessage ...@@ -45,7 +45,7 @@ module ChatMessage
private private
def message def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{time_measure}" "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{pretty_duration(duration)}"
end end
def humanized_status def humanized_status
...@@ -84,9 +84,5 @@ module ChatMessage ...@@ -84,9 +84,5 @@ module ChatMessage
def pipeline_link def pipeline_link
"[##{pipeline_id}](#{pipeline_url})" "[##{pipeline_id}](#{pipeline_url})"
end end
def time_measure
'second'.pluralize(duration)
end
end end
end end
...@@ -512,14 +512,8 @@ class Repository ...@@ -512,14 +512,8 @@ class Repository
delegate :tag_names, to: :raw_repository delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: [] cache_method :tag_names, fallback: []
def branch_count delegate :branch_count, :tag_count, to: :raw_repository
branches.size
end
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
def avatar def avatar
...@@ -802,7 +796,7 @@ class Repository ...@@ -802,7 +796,7 @@ class Repository
} }
options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
Rugged::Commit.create(rugged, options) create_commit(options)
end end
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
...@@ -868,7 +862,7 @@ class Repository ...@@ -868,7 +862,7 @@ class Repository
tree: merge_index.write_tree(rugged), tree: merge_index.write_tree(rugged),
) )
commit_id = Rugged::Commit.create(rugged, actual_options) commit_id = create_commit(actual_options)
merge_request.update(in_progress_merge_commit_sha: commit_id) merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id commit_id
end end
...@@ -891,12 +885,11 @@ class Repository ...@@ -891,12 +885,11 @@ class Repository
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, create_commit(message: commit.revert_message(user),
message: commit.revert_message(user), author: committer,
author: committer, committer: committer,
committer: committer, tree: revert_tree_id,
tree: revert_tree_id, parents: [start_commit.sha])
parents: [start_commit.sha])
end end
end end
...@@ -915,16 +908,15 @@ class Repository ...@@ -915,16 +908,15 @@ class Repository
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, create_commit(message: commit.message,
message: commit.message, author: {
author: { email: commit.author_email,
email: commit.author_email, name: commit.author_name,
name: commit.author_name, time: commit.authored_date
time: commit.authored_date },
}, committer: committer,
committer: committer, tree: cherry_pick_tree_id,
tree: cherry_pick_tree_id, parents: [start_commit.sha])
parents: [start_commit.sha])
end end
end end
...@@ -932,7 +924,7 @@ class Repository ...@@ -932,7 +924,7 @@ class Repository
GitOperationService.new(user, self).with_branch(branch_name) do GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) create_commit(params.merge(author: committer, committer: committer))
end end
end end
...@@ -1228,6 +1220,12 @@ class Repository ...@@ -1228,6 +1220,12 @@ class Repository
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end end
def create_commit(params = {})
params[:message].delete!("\r")
Rugged::Commit.create(rugged, params)
end
def repository_storage_path def repository_storage_path
@project.repository_storage_path @project.repository_storage_path
end end
......
class SnippetBlob class SnippetBlob
include Linguist::BlobHelper include BlobLike
attr_reader :snippet attr_reader :snippet
...@@ -28,32 +28,4 @@ class SnippetBlob ...@@ -28,32 +28,4 @@ class SnippetBlob
Banzai.render_field(snippet, :content) Banzai.render_field(snippet, :content)
end end
def mode
nil
end
def binary?
false
end
def load_all_data!(repository)
# No-op
end
def lfs_pointer?
false
end
def lfs_oid
nil
end
def lfs_size
nil
end
def truncated?
false
end
end end
...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -3,11 +3,16 @@ class PersonalSnippetPolicy < BasePolicy
can! :read_personal_snippet if @subject.public? can! :read_personal_snippet if @subject.public?
return unless @user return unless @user
if @subject.public?
can! :comment_personal_snippet
end
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
can! :comment_personal_snippet
end end
unless @user.external? unless @user.external?
...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -16,6 +21,7 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
can! :comment_personal_snippet
end end
end end
end end
...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity ...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end end
end end
expose :created_at
expose :tag expose :tag
expose :last? expose :last?
expose :user, using: UserEntity expose :user, using: UserEntity
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity expose :deployable, using: BuildEntity
......
class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
...@@ -4,7 +4,10 @@ module Projects ...@@ -4,7 +4,10 @@ module Projects
key = accessible_keys.find_by(id: params[:key_id] || params[:id]) key = accessible_keys.find_by(id: params[:key_id] || params[:id])
return unless key return unless key
project.deploy_keys << key unless project.deploy_keys.include?(key)
project.deploy_keys << key
end
key key
end end
......
module Projects
class UploadService < BaseService
def initialize(project, file)
@project, @file = project, file
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
end
class UploadService
def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class
end
def execute
return nil unless @file && @file.size <= max_attachment_size
uploader = @uploader_class.new(@model)
uploader.store!(@file)
uploader.to_h
end
private
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
end
...@@ -30,8 +30,4 @@ class ArtifactUploader < GitlabUploader ...@@ -30,8 +30,4 @@ class ArtifactUploader < GitlabUploader
def filename def filename
file.try(:filename) file.try(:filename)
end end
def exists?
file.try(:exists?)
end
end end
...@@ -26,11 +26,11 @@ class FileUploader < GitlabUploader ...@@ -26,11 +26,11 @@ class FileUploader < GitlabUploader
File.join(CarrierWave.root, base_dir, model.path_with_namespace) File.join(CarrierWave.root, base_dir, model.path_with_namespace)
end end
attr_accessor :project attr_accessor :model
attr_reader :secret attr_reader :secret
def initialize(project, secret = nil) def initialize(model, secret = nil)
@project = project @model = model
@secret = secret || generate_secret @secret = secret || generate_secret
end end
...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader ...@@ -38,10 +38,6 @@ class FileUploader < GitlabUploader
File.join(dynamic_path_segment, @secret) File.join(dynamic_path_segment, @secret)
end end
def model
project
end
def relative_path def relative_path
self.file.path.sub("#{dynamic_path_segment}/", '') self.file.path.sub("#{dynamic_path_segment}/", '')
end end
......
...@@ -33,4 +33,8 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -33,4 +33,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
def relative_path def relative_path
self.file.path.sub("#{root}/", '') self.file.path.sub("#{root}/", '')
end end
def exists?
file.try(:exists?)
end
end end
...@@ -9,10 +9,6 @@ class LfsObjectUploader < GitlabUploader ...@@ -9,10 +9,6 @@ class LfsObjectUploader < GitlabUploader
"#{Gitlab.config.lfs.storage_path}/tmp/cache" "#{Gitlab.config.lfs.storage_path}/tmp/cache"
end end
def exists?
file.try(:exists?)
end
def filename def filename
model.oid[4..-1] model.oid[4..-1]
end end
......
class PersonalFileUploader < FileUploader
def self.dynamic_path_segment(model)
File.join(CarrierWave.root, model_path(model))
end
private
def secure_url
File.join(self.class.model_path(model), secret, file.filename)
end
def self.model_path(model)
File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s)
end
end
...@@ -177,11 +177,7 @@ ...@@ -177,11 +177,7 @@
.panel-body .panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
%ul = render 'users/deletion_guidance', user: @user
%li All user content like authored issues, snippets, comments will be removed
- rp = @user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
%br %br
= link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.diff-file.file-holder .diff-file.file-holder
.js-file-title.file-title .js-file-title.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion) = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion)
.diff-content.code.js-syntax-highlight .diff-content.code.js-syntax-highlight
%table %table
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= discussion.author.to_reference = discussion.author.to_reference
started a discussion started a discussion
- url = discussion_diff_path(discussion) - url = discussion_path(discussion)
- if discussion.for_commit? && @noteable != discussion.noteable - if discussion.for_commit? && @noteable != discussion.noteable
on on
- commit = discussion.noteable - commit = discussion.noteable
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments]) do = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span %span
Pipelines Pipelines
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
- project = @target_project || @project - project = @target_project || @project
- if current_user - if current_user
:javascript :javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path(project)}";
- content_for :header_content do - content_for :header_content do
.js-dropdown-menu-projects .js-dropdown-menu-projects
......
...@@ -118,11 +118,7 @@ ...@@ -118,11 +118,7 @@
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p %p
Deleting an account has the following effects: Deleting an account has the following effects:
%ul = render 'users/deletion_guidance', user: current_user
%li All user content like authored issues, snippets, comments will be removed
- rp = current_user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
......
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
%tr.tree-item{ 'data-link' => path_to_directory } %tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name %td.tree-item-file-name
= tree_icon('folder', '755', directory.name) = tree_icon('folder', '755', directory.name)
%span.str-truncated = link_to path_to_directory do
= link_to directory.name, path_to_directory %span.str-truncated= directory.name
%td %td
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head"
.top-block.row-content-block.clearfix = render "projects/builds/header", show_controls: false
.pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
.tree-holder .tree-holder
.nav-block
.tree-controls
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
%ul.breadcrumb.repo-breadcrumb
%li
= link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path|
%li
= link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path)
.tree-content-holder .tree-content-holder
%table.table.tree-table %table.table.tree-table
%thead %thead
......
...@@ -6,18 +6,14 @@ ...@@ -6,18 +6,14 @@
%li %li
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path = @project.path
- tree_breadcrumbs(@tree, 6) do |title, path, part_path| - path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li %li
- if path - if path == @path
- if path.end_with?(@path) = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, path)) do
= link_to namespace_project_blob_path(@project.namespace, @project, path) do %strong= title
%strong
= truncate(title, length: 40)
- else
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else - else
= link_to title, '#' = link_to title, namespace_project_tree_path(@project.namespace, @project, tree_join(@ref, path))
= render_lock_icon(part_path)
%ul.blob-commit-info.hidden-xs %ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
...@@ -26,5 +22,4 @@ ...@@ -26,5 +22,4 @@
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
%article.file-holder %article.file-holder
= render "projects/blob/header", blob: blob = render "projects/blob/header", blob: blob
= render 'projects/blob/content', blob: blob = render 'projects/blob/content', blob: blob
- blame = local_assigns.fetch(:blame, false) - blame = local_assigns.fetch(:blame, false)
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
.file-header-content = render 'projects/blob/header_content', blob: blob
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob unless blame = render 'projects/blob/viewer_switcher', blob: blob unless blame
......
.file-header-content
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
...@@ -28,8 +28,9 @@ ...@@ -28,8 +28,9 @@
":value" => "issue.assignee.id", ":value" => "issue.assignee.id",
"v-if" => "issue.assignee" } "v-if" => "issue.assignee" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee Select assignee
= icon("chevron-down") = icon("chevron-down")
......
- show_controls = local_assigns.fetch(:show_controls, true)
- pipeline = @build.pipeline - pipeline = @build.pipeline
.content-block.build-header.top-area .content-block.build-header.top-area
.header-content .header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
Job %strong
%strong.js-build-id ##{@build.id} Job
= link_to namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id' do
\##{@build.id}
in pipeline in pipeline
= link_to pipeline_path(pipeline) do = link_to pipeline_path(pipeline) do
%strong ##{pipeline.id} %strong ##{pipeline.id}
...@@ -15,13 +18,16 @@ ...@@ -15,13 +18,16 @@
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code %code
= @build.ref = @build.ref
- if @build.user
= render "user" = render "projects/builds/user" if @build.user
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
.nav-controls
- if can?(current_user, :create_issue, @project) && @build.failed? - if show_controls
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' .nav-controls
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } - if can?(current_user, :update_build, @build) && @build.retryable?
= icon('angle-double-left') = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
- if @build.merge_request - if @build.merge_request
%p.build-detail-row %p.build-detail-row
%span.build-light-text Merge Request: %span.build-light-text Merge Request:
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request) = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold'
- if @build.duration - if @build.duration
%p.build-detail-row %p.build-detail-row
%span.build-light-text Duration: %span.build-light-text Duration:
......
...@@ -61,19 +61,20 @@ ...@@ -61,19 +61,20 @@
%span.commit-info.branches %span.commit-info.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
- if @commit.status - if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info .well-segment.pipeline-info
.status-icon-container{ class: "ci-status-icon-#{@commit.status}" } .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
= link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do = link_to namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) do
= ci_icon_for_status(@commit.status) = ci_icon_for_status(last_pipeline.status)
Pipeline Pipeline
= link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace" = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id), class: "monospace"
= ci_label_for_status(@commit.status) = ci_label_for_status(last_pipeline.status)
- if @commit.latest_pipeline.stages.any? - if last_pipeline.stages.any?
.mr-widget-pipeline-graph .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph' = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
in in
= time_interval_in_words @commit.pipelines.total_duration = time_interval_in_words last_pipeline.duration
:javascript :javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
- @no_container = true - @no_container = true
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width'
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description - page_description @commit.description
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: container_class } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
- if @commit.status - if @commit.status
= render "ci_menu" = render "ci_menu"
......
...@@ -35,6 +35,6 @@ ...@@ -35,6 +35,6 @@
- else - else
= diff_line_content(line.text) = diff_line_content(line.text)
- if line_discussions - if line_discussions&.any?
- discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?)) - discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?))
= render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded = render "discussions/diff_discussion", discussions: line_discussions, expanded: discussion_expanded
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
git init git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add . git add .
git commit git commit -m "Initial commit"
git push -u origin master git push -u origin master
%fieldset %fieldset
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring') = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head" = render "projects/pipelines/head"
.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" } #js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.top-area .top-area
.row .row
.col-sm-6 .col-sm-6
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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