Commit e6c87595 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'master' into 'alerts-for-built-in-metrics-ee'

# Conflicts:
#   db/schema.rb
parents fb56b670 0f16b87f
...@@ -33,6 +33,15 @@ rules: ...@@ -33,6 +33,15 @@ rules:
- error - error
- max: 1 - max: 1
promise/catch-or-return: error promise/catch-or-return: error
no-param-reassign:
- error
- props: true
ignorePropertyModificationsFor:
- "acc" # for reduce accumulators
- "accumulator" # for reduce accumulators
- "el" # for DOM elements
- "element" # for DOM elements
- "state" # for Vuex mutations
no-underscore-dangle: no-underscore-dangle:
- error - error
- allow: - allow:
......
# Backend Maintainers are the default for all ruby files
*.rb @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
*.rake @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
# Feature specific owners
/ee/lib/gitlab/code_owners/ @reprazent
...@@ -71,7 +71,7 @@ gem 'u2f', '~> 0.2.1' ...@@ -71,7 +71,7 @@ gem 'u2f', '~> 0.2.1'
gem 'validates_hostname', '~> 1.0.6' gem 'validates_hostname', '~> 1.0.6'
# Browser detection # Browser detection
gem 'browser', '~> 2.2' gem 'browser', '~> 2.5'
# GPG # GPG
gem 'gpgme' gem 'gpgme'
...@@ -110,7 +110,9 @@ gem 'kaminari', '~> 1.0' ...@@ -110,7 +110,9 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.8.8' gem 'hamlit', '~> 2.8.8'
# Files attachments # Files attachments
gem 'carrierwave', '~> 1.2' # Locked until https://github.com/carrierwaveuploader/carrierwave/pull/2332/files is merged.
# config/initializers/carrierwave_patch.rb can be removed once that change is released.
gem 'carrierwave', '= 1.2.3'
gem 'mini_magick' gem 'mini_magick'
# Drag and Drop UI # Drag and Drop UI
......
...@@ -98,7 +98,7 @@ GEM ...@@ -98,7 +98,7 @@ GEM
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.2.0) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -1021,12 +1021,12 @@ DEPENDENCIES ...@@ -1021,12 +1021,12 @@ DEPENDENCIES
bootsnap (~> 1.3) bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.5)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.15) capybara (~> 2.15)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2) carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
......
...@@ -101,7 +101,7 @@ GEM ...@@ -101,7 +101,7 @@ GEM
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
browser (2.2.0) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -1030,12 +1030,12 @@ DEPENDENCIES ...@@ -1030,12 +1030,12 @@ DEPENDENCIES
bootsnap (~> 1.3) bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 4.2) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.5)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.15) capybara (~> 2.15)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2) carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
......
...@@ -33,7 +33,7 @@ Documentation: ...@@ -33,7 +33,7 @@ Documentation:
http://doc.gitlab.com/ee/ http://doc.gitlab.com/ee/
To upgrade from CE, just perform a normal upgrade, but use an EE package: To upgrade from CE, just perform a normal upgrade, but use an EE package:
https://about.gitlab.com/update/#ee https://about.gitlab.com/upgrade/
If you need help with your GitLab installation and for any technical questions please see the [support page](https://about.gitlab.com/support/) If you need help with your GitLab installation and for any technical questions please see the [support page](https://about.gitlab.com/support/)
......
app/assets/images/auth_buttons/azure_64.png

695 Bytes | W: | H:

app/assets/images/auth_buttons/azure_64.png

199 Bytes | W: | H:

app/assets/images/auth_buttons/azure_64.png
app/assets/images/auth_buttons/azure_64.png
app/assets/images/auth_buttons/azure_64.png
app/assets/images/auth_buttons/azure_64.png
  • 2-up
  • Swipe
  • Onion skin
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Vue from 'vue'; import Vue from 'vue';
import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale'; import { s__, sprintf } from '../locale';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels'; import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import applications from './components/applications.vue';
...@@ -66,6 +62,7 @@ export default class Clusters { ...@@ -66,6 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
...@@ -129,7 +126,8 @@ export default class Clusters { ...@@ -129,7 +126,8 @@ export default class Clusters {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.poll.makeRequest(); this.poll.makeRequest();
} else { } else {
this.service.fetchData() this.service
.fetchData()
.then(data => this.handleSuccess(data)) .then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError()); .catch(() => Clusters.handleError());
} }
...@@ -177,15 +175,21 @@ export default class Clusters { ...@@ -177,15 +175,21 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) { checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap) const appTitles = Object.keys(newApplicationMap)
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && .filter(
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && appId =>
prevApplicationMap[appId].status !== null) newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null,
)
.map(appId => newApplicationMap[appId].title); .map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) { if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), { const text = sprintf(
appList: appTitles.join(', '), s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
}); {
appList: appTitles.join(', '),
},
);
Flash(text, 'notice', this.successApplicationContainer); Flash(text, 'notice', this.successApplicationContainer);
} }
} }
...@@ -218,13 +222,18 @@ export default class Clusters { ...@@ -218,13 +222,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId, data.params) this.service
.installApplication(appId, data.params)
.then(() => { .then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
}) })
.catch(() => { .catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); this.store.updateAppProperty(
appId,
'requestReason',
s__('ClusterIntegration|Request to begin installing failed'),
);
}); });
} }
......
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons'; import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
export default () => { export default () => {
const clusterList = document.querySelector('.js-clusters-list'); const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer(); initDismissableCallout('.gcp-signup-offer');
// The empty state won't have a clusterList // The empty state won't have a clusterList
if (clusterList) { if (clusterList) {
......
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
v-else v-else
role="button" role="button"
class="fa fa-times dropdown-input-search" class="fa fa-times dropdown-input-search"
@click="clearSearch" @click.stop.prevent="clearSearch"
></i> ></i>
</div> </div>
<div class="dropdown-content"> <div class="dropdown-content">
......
...@@ -107,7 +107,6 @@ export default { ...@@ -107,7 +107,6 @@ export default {
}, },
[types.EXPAND_ALL_FILES](state) { [types.EXPAND_ALL_FILES](state) {
// eslint-disable-next-line no-param-reassign
state.diffFiles = state.diffFiles.map(file => ({ state.diffFiles = state.diffFiles.map(file => ({
...file, ...file,
collapsed: false, collapsed: false,
......
...@@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Flash from '~/flash'; import Flash from '~/flash';
export default function gcpSignupOffer() { export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector('.gcp-signup-offer'); const alertEl = document.querySelector(alertSelector);
if (!alertEl) { if (!alertEl) {
return; return;
} }
......
...@@ -65,8 +65,8 @@ export const hideMenu = (el) => { ...@@ -65,8 +65,8 @@ export const hideMenu = (el) => {
const parentEl = el.parentNode; const parentEl = el.parentNode;
el.style.display = ''; // eslint-disable-line no-param-reassign el.style.display = '';
el.style.transform = ''; // eslint-disable-line no-param-reassign el.style.transform = '';
el.classList.remove(IS_ABOVE_CLASS); el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS); parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import { normalizeJob } from './utils'; import { normalizeJob } from './utils';
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request'; import mergeRequestMutation from './mutations/merge_request';
......
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { sortTree } from '../utils'; import { sortTree } from '../utils';
import { diffModes } from '../../constants'; import { diffModes } from '../../constants';
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select'; import projectSelect from '~/project_select';
import UsagePingPayload from './usage_ping_payload';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
projectSelect(); projectSelect();
new UsagePingPayload(
document.querySelector('.js-usage-ping-payload-trigger'),
document.querySelector('.js-usage-ping-payload'),
).init();
}); });
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
import flash from '../../../flash';
export default class UsagePingPayload {
constructor(trigger, container) {
this.trigger = trigger;
this.container = container;
this.isVisible = false;
this.isInserted = false;
}
init() {
this.spinner = this.trigger.querySelector('.js-spinner');
this.text = this.trigger.querySelector('.js-text');
this.trigger.addEventListener('click', event => {
event.preventDefault();
if (this.isVisible) return this.hidePayload();
return this.requestPayload();
});
}
requestPayload() {
if (this.isInserted) return this.showPayload();
this.spinner.classList.add('d-inline');
return axios
.get(this.container.dataset.endpoint, {
responseType: 'text',
})
.then(({ data }) => {
this.spinner.classList.remove('d-inline');
this.insertPayload(data);
})
.catch(() => {
this.spinner.classList.remove('d-inline');
flash(__('Error fetching usage ping data.'));
});
}
hidePayload() {
this.isVisible = false;
this.container.classList.add('d-none');
this.text.textContent = __('Preview payload');
}
showPayload() {
this.isVisible = true;
this.container.classList.remove('d-none');
this.text.textContent = __('Hide payload');
}
insertPayload(data) {
this.isInserted = true;
this.container.innerHTML = data;
this.showPayload();
}
}
import initUsagePing from './usage_ping';
document.addEventListener('DOMContentLoaded', initUsagePing);
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
import flash from '../../../flash';
export default function UsagePing() {
const el = document.querySelector('.usage-data');
axios.get(el.dataset.endpoint, {
responseType: 'text',
}).then(({ data }) => {
el.innerHTML = data;
}).catch(() => flash(__('Error fetching usage ping data.')));
}
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project'; import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation'; import ShortcutsNavigation from '../../shortcuts_navigation';
...@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
]; ];
if (newClusterViews.indexOf(page) > -1) { if (newClusterViews.indexOf(page) > -1) {
gcpSignupOffer(); initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns(); initGkeDropdowns();
} }
......
...@@ -13,45 +13,57 @@ export default class Project { ...@@ -13,45 +13,57 @@ export default class Project {
constructor() { constructor() {
const $cloneOptions = $('ul.clone-options-dropdown'); const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone'); const $projectCloneField = $('#project_clone');
const $cloneBtnText = $('a.clone-dropdown-btn span'); const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
const selectedCloneOption = $cloneBtnText.text().trim(); const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) { if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
} }
$('a', $cloneOptions).on('click', (e) => { $('a', $cloneOptions).on('click', e => {
e.preventDefault();
const $this = $(e.currentTarget); const $this = $(e.currentTarget);
const url = $this.attr('href'); const url = $this.attr('href');
const activeText = $this.find('.dropdown-menu-inner-title').text(); const cloneType = $this.data('cloneType');
e.preventDefault(); $('.is-active', $cloneOptions).removeClass('is-active');
$(`a[data-clone-type="${cloneType}"]`).each(function() {
const $el = $(this);
const activeText = $el.find('.dropdown-menu-inner-title').text();
const $container = $el.closest('.project-clone-holder');
const $label = $container.find('.js-clone-dropdown-label');
$('.is-active', $cloneOptions).not($this).removeClass('is-active'); $el.toggleClass('is-active');
$this.toggleClass('is-active'); $label.text(activeText);
$projectCloneField.val(url); });
$cloneBtnText.text(activeText);
$('#modal-geo-info').data({ $('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'), cloneUrlSecondary: $this.attr('href'),
cloneUrlPrimary: $this.data('primaryUrl') || '', cloneUrlPrimary: $this.data('primaryUrl') || '',
}); });
return $('.clone').text(url); $projectCloneField.val(url);
$('.js-git-empty .js-clone').text(url);
}); });
// Ref switcher // Ref switcher
Project.initRefSwitcher(); Project.initRefSwitcher();
$('.project-refs-select').on('change', function() { $('.project-refs-select').on('change', function() {
return $(this).parents('form').submit(); return $(this)
.parents('form')
.submit();
}); });
$('.hide-no-ssh-message').on('click', function(e) { $('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false'); Cookies.set('hide_no_ssh_message', 'false');
$(this).parents('.no-ssh-key-message').remove(); $(this)
.parents('.no-ssh-key-message')
.remove();
return e.preventDefault(); return e.preventDefault();
}); });
$('.hide-no-password-message').on('click', function(e) { $('.hide-no-password-message').on('click', function(e) {
Cookies.set('hide_no_password_message', 'false'); Cookies.set('hide_no_password_message', 'false');
$(this).parents('.no-password-message').remove(); $(this)
.parents('.no-password-message')
.remove();
return e.preventDefault(); return e.preventDefault();
}); });
$('.hide-shared-runner-limit-message').on('click', function(e) { $('.hide-shared-runner-limit-message').on('click', function(e) {
...@@ -70,7 +82,7 @@ export default class Project { ...@@ -70,7 +82,7 @@ export default class Project {
} }
static changeProject(url) { static changeProject(url) {
return window.location = url; return (window.location = url);
} }
static initRefSwitcher() { static initRefSwitcher() {
...@@ -85,14 +97,15 @@ export default class Project { ...@@ -85,14 +97,15 @@ export default class Project {
selected = $dropdown.data('selected'); selected = $dropdown.data('selected');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
data(term, callback) { data(term, callback) {
axios.get($dropdown.data('refsUrl'), { axios
params: { .get($dropdown.data('refsUrl'), {
ref: $dropdown.data('ref'), params: {
search: term, ref: $dropdown.data('ref'),
}, search: term,
}) },
.then(({ data }) => callback(data)) })
.catch(() => flash(__('An error occurred while getting projects'))); .then(({ data }) => callback(data))
.catch(() => flash(__('An error occurred while getting projects')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
......
...@@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index'; ...@@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities'; import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils'; import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges'; import GpgBadges from '~/gpg_badges';
import initReadMore from '~/read_more';
import Star from '../../../star'; import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown'; import notificationsDropdown from '../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initReadMore();
new Star(); // eslint-disable-line no-new new Star(); // eslint-disable-line no-new
notificationsDropdown(); notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new NotificationsForm(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new
new UserCallout({ // eslint-disable-line no-new // eslint-disable-next-line no-new
new UserCallout({
setCalloutPerProject: false, setCalloutPerProject: false,
className: 'js-autodevops-banner', className: 'js-autodevops-banner',
}); });
......
...@@ -132,10 +132,8 @@ export default { ...@@ -132,10 +132,8 @@ export default {
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') { if (prop === 'path') {
// eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
// eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
return accumulator; return accumulator;
......
/**
* ReadMore
*
* Adds "read more" functionality to elements.
*
* Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class
* "is-expanded" to the previous element in order to provide a click to expand functionality.
*
* This is useful for long text elements that you would like to truncate, especially for mobile.
*
* Example Markup
* <div class="read-more-container">
* <p>Some text that should be long enough to have to truncate within a specified container.</p>
* <p>This text will not appear in the container, as only the first line can be truncated.</p>
* <p>This should also not appear, if everything is working correctly!</p>
* </div>
* <button class="js-read-more-trigger">Read more</button>
*
*/
export default function initReadMore(triggerSelector = '.js-read-more-trigger') {
const triggerEls = document.querySelectorAll(triggerSelector);
if (!triggerEls) return;
triggerEls.forEach(triggerEl => {
const targetEl = triggerEl.previousElementSibling;
if (!targetEl) {
return;
}
triggerEl.addEventListener(
'click',
e => {
targetEl.classList.add('is-expanded');
e.target.remove();
},
{ once: true },
);
});
}
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
...@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => { ...@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => { response.headers.forEach((value, key) => {
headers[key] = value; headers[key] = value;
}); });
// eslint-disable-next-line no-param-reassign
response.headers = headers; response.headers = headers;
}); });
}); });
...@@ -66,3 +66,4 @@ ...@@ -66,3 +66,4 @@
@import 'framework/ci_variable_list'; @import 'framework/ci_variable_list';
@import 'framework/feature_highlight'; @import 'framework/feature_highlight';
@import 'framework/terms'; @import 'framework/terms';
@import 'framework/read_more';
...@@ -229,8 +229,8 @@ ...@@ -229,8 +229,8 @@
svg { svg {
margin-bottom: 1px; margin-bottom: 1px;
height: 18px; height: $default-icon-size;
width: 18px; width: $default-icon-size;
border-radius: 50%; border-radius: 50%;
path { path {
......
...@@ -149,7 +149,8 @@ ...@@ -149,7 +149,8 @@
&.btn-success, &.btn-success,
&.btn-new, &.btn-new,
&.btn-create, &.btn-create,
&.btn-save { &.btn-save,
&.btn-register {
@include btn-green; @include btn-green;
} }
...@@ -172,8 +173,7 @@ ...@@ -172,8 +173,7 @@
} }
&.btn-info, &.btn-info,
&.btn-primary, &.btn-primary {
&.btn-register {
@include btn-blue; @include btn-blue;
} }
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
.btn-terminal { .btn-terminal {
svg { svg {
height: 14px; height: 14px;
width: 18px; width: $default-icon-size;
} }
} }
......
...@@ -216,8 +216,8 @@ ...@@ -216,8 +216,8 @@
vertical-align: inherit; vertical-align: inherit;
img { img {
height: 18px; height: $default-icon-size;
width: 18px; width: $default-icon-size;
} }
} }
......
...@@ -44,12 +44,8 @@ ...@@ -44,12 +44,8 @@
.project-repo-buttons { .project-repo-buttons {
display: block; display: block;
.count-buttons .btn { .count-buttons .count-badge {
margin: 0 10px; margin-top: $gl-padding-8;
}
.count-buttons .count-with-arrow {
display: none;
} }
} }
} }
......
.read-more-container {
@include media-breakpoint-down(md) {
&:not(.is-expanded) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> * {
display: inline;
}
}
}
}
...@@ -56,8 +56,8 @@ ...@@ -56,8 +56,8 @@
&, &,
.toggle-icon-svg { .toggle-icon-svg {
width: 18px; width: $default-icon-size;
height: 18px; height: $default-icon-size;
} }
.toggle-icon-svg { .toggle-icon-svg {
......
...@@ -252,7 +252,7 @@ $container-text-max-width: 540px; ...@@ -252,7 +252,7 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$border-radius-default: 4px; $border-radius-default: 4px;
$border-radius-small: 2px; $border-radius-small: 2px;
$settings-icon-size: 18px; $default-icon-size: 18px;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$btn-side-margin: 10px; $btn-side-margin: 10px;
$btn-sm-side-margin: 7px; $btn-sm-side-margin: 7px;
...@@ -274,6 +274,7 @@ $performance-bar-height: 35px; ...@@ -274,6 +274,7 @@ $performance-bar-height: 35px;
$flash-height: 52px; $flash-height: 52px;
$context-header-height: 60px; $context-header-height: 60px;
$breadcrumb-min-height: 48px; $breadcrumb-min-height: 48px;
$project-title-row-height: 24px;
// EE-only CSS variables START // EE-only CSS variables START
$system-header-height: 35px; $system-header-height: 35px;
......
...@@ -4,3 +4,7 @@ ...@@ -4,3 +4,7 @@
padding-bottom: 46px; padding-bottom: 46px;
} }
} }
.usage-data {
max-height: 400px;
}
...@@ -749,6 +749,10 @@ ...@@ -749,6 +749,10 @@
left: $gl-padding; left: $gl-padding;
} }
.dropdown-input .dropdown-input-search {
pointer-events: all;
}
.diff-changed-file { .diff-changed-file {
display: flex; display: flex;
padding-top: 8px; padding-top: 8px;
......
...@@ -100,6 +100,22 @@ ...@@ -100,6 +100,22 @@
p { p {
margin: 0; margin: 0;
} }
.omniauth-btn {
margin-bottom: $gl-padding;
width: 48%;
padding: $gl-padding-8;
@include media-breakpoint-down(md) {
width: 100%;
}
img {
width: $default-icon-size;
height: $default-icon-size;
margin-right: $gl-padding;
}
}
} }
.new-session-tabs { .new-session-tabs {
...@@ -169,10 +185,6 @@ ...@@ -169,10 +185,6 @@
} }
} }
label {
font-weight: $gl-font-weight-normal;
}
.submit-container { .submit-container {
margin-top: 16px; margin-top: 16px;
} }
...@@ -200,15 +212,6 @@ ...@@ -200,15 +212,6 @@
} }
} }
.oauth-image-link {
margin-right: 10px;
img {
width: 32px;
height: 32px;
}
}
.devise-layout-html { .devise-layout-html {
margin: 0; margin: 0;
padding: 0; padding: 0;
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
.project-feature-controls { .project-feature-controls {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 8px 0; margin: $gl-padding-8 0;
max-width: 432px; max-width: 432px;
.toggle-wrapper { .toggle-wrapper {
...@@ -144,12 +144,8 @@ ...@@ -144,12 +144,8 @@
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
border-bottom: 1px solid $border-color;
@include media-breakpoint-up(sm) {
border-bottom: 1px solid $border-color;
}
.project-avatar,
.group-avatar { .group-avatar {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
...@@ -175,7 +171,6 @@ ...@@ -175,7 +171,6 @@
} }
} }
.project-home-desc,
.group-home-desc { .group-home-desc {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
...@@ -199,6 +194,62 @@ ...@@ -199,6 +194,62 @@
} }
} }
.project-home-panel {
padding-top: $gl-padding-8;
padding-bottom: $gl-padding-24;
.project-title-row {
margin-right: $gl-padding-8;
}
.project-avatar {
width: $project-title-row-height;
height: $project-title-row-height;
flex-shrink: 0;
flex-basis: $project-title-row-height;
margin: 0 $gl-padding-8 0 0;
}
.project-title {
font-size: 20px;
line-height: $project-title-row-height;
font-weight: bold;
}
.project-metadata {
font-weight: normal;
font-size: 14px;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
.icon {
margin-right: $gl-padding-4;
font-size: 16px;
}
.project-visibility,
.project-license,
.project-tag-list {
margin-right: $gl-padding-8;
}
.project-license {
.btn {
line-height: 0;
border-width: 0;
}
}
.project-tag-list,
.project-license {
.icon {
position: relative;
top: 2px;
}
}
}
}
.nav > .project-repo-buttons { .nav > .project-repo-buttons {
margin-top: 0; margin-top: 0;
} }
...@@ -206,8 +257,6 @@ ...@@ -206,8 +257,6 @@
.project-repo-buttons, .project-repo-buttons,
.group-buttons { .group-buttons {
.btn { .btn {
padding: 3px 10px;
&:last-child { &:last-child {
margin-left: 0; margin-left: 0;
} }
...@@ -222,11 +271,15 @@ ...@@ -222,11 +271,15 @@
.fa-caret-down { .fa-caret-down {
margin-left: 3px; margin-left: 3px;
&.dropdown-btn-icon {
margin-left: 0;
}
} }
} }
.project-action-button { .project-action-button {
margin: 15px 5px 0; margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top; vertical-align: top;
} }
...@@ -243,82 +296,45 @@ ...@@ -243,82 +296,45 @@
.count-buttons { .count-buttons {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-top: 15px; margin-top: $gl-padding;
}
.project-clone-holder { .count-badge {
display: inline-block; height: $input-height;
margin: 15px 5px 0 0;
input { .icon {
height: 28px; top: -1px;
}
} }
}
.count-with-arrow { .count-badge-count,
display: inline-block; .count-badge-button {
position: relative; border: 1px solid $border-color;
margin-left: 4px; line-height: 1;
}
.arrow { .count,
&::before { .count-badge-button {
content: ''; color: $gl-text-color;
display: inline-block; }
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 50%;
left: 0;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $count-arrow-border;
pointer-events: none;
}
&::after { .count-badge-count {
content: ''; padding: 0 12px;
position: absolute; border-right: 0;
width: 0; border-radius: $border-radius-base 0 0 $border-radius-base;
height: 0; background: $gray-light;
border-color: transparent;
border-style: solid;
top: 50%;
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
pointer-events: none;
}
} }
.count { .count-badge-button {
@include btn-white; border-radius: 0 $border-radius-base $border-radius-base 0;
display: inline-block; }
background: $white-light; }
border-radius: 2px;
border-width: 1px;
border-style: solid;
font-size: 13px;
font-weight: $gl-font-weight-bold;
line-height: 13px;
letter-spacing: 0.4px;
padding: 6px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
background-image: none;
white-space: nowrap;
margin: 0 10px 0 4px;
a { .project-clone-holder {
color: inherit; display: inline-block;
} margin: $gl-padding $gl-padding-8 0 0;
&:hover { input {
background: $white-light; height: $input-height;
}
} }
} }
...@@ -333,6 +349,14 @@ ...@@ -333,6 +349,14 @@
min-width: 320px; min-width: 320px;
} }
} }
.mobile-git-clone {
margin-top: $gl-padding-8;
.dropdown-menu-inner-content {
@extend .monospace;
}
}
} }
.split-one { .split-one {
...@@ -512,7 +536,6 @@ ...@@ -512,7 +536,6 @@
.controls { .controls {
margin-left: auto; margin-left: auto;
} }
} }
.choose-template { .choose-template {
...@@ -575,7 +598,7 @@ ...@@ -575,7 +598,7 @@
flex-wrap: wrap; flex-wrap: wrap;
.btn { .btn {
padding: 8px; padding: $gl-padding-8;
margin-right: 10px; margin-right: 10px;
} }
...@@ -652,7 +675,7 @@ ...@@ -652,7 +675,7 @@
left: -10px; left: -10px;
top: 50%; top: 50%;
z-index: 10; z-index: 10;
padding: 8px 0; padding: $gl-padding-8 0;
text-align: center; text-align: center;
background-color: $white-light; background-color: $white-light;
color: $gl-text-color-tertiary; color: $gl-text-color-tertiary;
...@@ -666,7 +689,7 @@ ...@@ -666,7 +689,7 @@
left: 50%; left: 50%;
top: 0; top: 0;
transform: translateX(-50%); transform: translateX(-50%);
padding: 0 8px; padding: 0 $gl-padding-8;
} }
} }
...@@ -700,17 +723,51 @@ ...@@ -700,17 +723,51 @@
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.nav { .scrolling-tabs-container {
margin-top: $gl-padding-8; .scrolling-tabs {
margin-bottom: $gl-padding-8; margin-top: $gl-padding-8;
margin-bottom: $gl-padding-8;
flex-wrap: wrap;
border-bottom: 0;
}
.fade-left,
.fade-right {
top: 0;
height: 100%;
.fa {
top: 50%;
margin-top: -$gl-padding-8;
}
}
.nav {
flex-basis: 100%;
+ .nav {
margin: $gl-padding-8 0;
}
}
@include media-breakpoint-down(md) {
flex-direction: column;
.nav {
flex-wrap: nowrap;
}
.nav:first-child {
margin-right: $gl-padding-8;
}
}
}
.nav {
> li { > li {
display: inline-block; display: inline-block;
margin-top: $gl-padding-4;
margin-bottom: $gl-padding-4;
&:not(:last-child) { &:not(:last-child) {
margin-right: $gl-padding; margin-right: $gl-padding;
...@@ -733,13 +790,17 @@ ...@@ -733,13 +790,17 @@
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: $gl-btn-line-height; line-height: $gl-btn-line-height;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
white-space: nowrap;
} }
.stat-link { .stat-link {
border-bottom: 0;
&:hover, &:hover,
&:focus { &:focus {
color: $gl-text-color; color: $gl-text-color;
text-decoration: underline; text-decoration: underline;
border-bottom: 0;
} }
} }
...@@ -869,7 +930,7 @@ pre.light-well { ...@@ -869,7 +930,7 @@ pre.light-well {
} }
.git-clone-holder { .git-clone-holder {
width: 380px; width: 320px;
.btn-clipboard { .btn-clipboard {
border: 1px solid $border-color; border: 1px solid $border-color;
......
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
.settings-list-icon { .settings-list-icon {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: $settings-icon-size; font-size: $default-icon-size;
line-height: 42px; line-height: 42px;
} }
......
...@@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base ...@@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base
def append_info_to_payload(payload) def append_info_to_payload(payload)
super super
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
logged_user = auth_user logged_user = auth_user
......
module SendFileUpload module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment if attachment
redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } # Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for
# this to work. However, this override works with AWS.
redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}",
"response-content-type" => guess_content_type(attachment) }
# By default, Rails will send uploads with an extension of .js with a # By default, Rails will send uploads with an extension of .js with a
# content-type of text/javascript, which will trigger Rails' # content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection. # cross-origin JavaScript protection.
...@@ -18,4 +22,14 @@ module SendFileUpload ...@@ -18,4 +22,14 @@ module SendFileUpload
redirect_to file_upload.url(**redirect_params) redirect_to file_upload.url(**redirect_params)
end end
end end
def guess_content_type(filename)
types = MIME::Types.type_for(filename)
if types.present?
types.first.content_type
else
"application/octet-stream"
end
end
end end
# frozen_string_literal: true # frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
before_action :authenticate_usage_ping_enabled_or_admin!
def index def index
if Gitlab::CurrentSettings.usage_ping_enabled if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
...@@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon ...@@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results) @cohorts = CohortsSerializer.new.represent(cohorts_results)
end end
end end
def authenticate_usage_ping_enabled_or_admin!
render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
end
end end
...@@ -159,7 +159,8 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -159,7 +159,8 @@ class Projects::ClustersController < Projects::ApplicationController
:namespace, :namespace,
:api_url, :api_url,
:token, :token,
:ca_cert :ca_cert,
:authorization_type
]).merge( ]).merge(
provider_type: :user, provider_type: :user,
platform_type: :kubernetes platform_type: :kubernetes
......
class Projects::RefsController < Projects::ApplicationController class Projects::RefsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include TreeHelper include TreeHelper
include PathLocksHelper
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :validate_ref_id before_action :validate_ref_id
...@@ -37,58 +36,47 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -37,58 +36,47 @@ class Projects::RefsController < Projects::ApplicationController
end end
def logs_tree def logs_tree
@offset = if params[:offset].present? summary = ::Gitlab::TreeSummary.new(
params[:offset].to_i @commit,
else @project,
0 path: @path,
end offset: params[:offset],
limit: 25
)
@limit = 25 @logs, commits = summary.summarize
@more_log_url = more_url(summary.next_offset) if summary.more?
@path = params[:path]
contents = []
contents.push(*tree.trees)
contents.push(*tree.blobs)
contents.push(*tree.submodules)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
@logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
commit_path = project_commit_path(@project, last_commit) if last_commit
path_lock = @project.find_path_lock(file)
{
file_name: content.name,
commit: last_commit,
type: content.type,
commit_path: commit_path,
lock_label: path_lock && text_label_for_lock(path_lock, file)
}
end
end
offset = @offset + @limit
if contents.size > offset
@more_log_url = logs_file_project_ref_path(@project, @ref, @path || '', offset: offset)
end
respond_to do |format| respond_to do |format|
format.html { render_404 } format.html { render_404 }
format.json do format.json do
response.headers["More-Logs-Url"] = @more_log_url response.headers["More-Logs-Url"] = @more_log_url if summary.more?
render json: @logs render json: @logs
end end
format.js
# The commit titles must be rendered and redacted before being shown.
# Doing it here allows us to apply performance optimizations that avoid
# N+1 problems
format.js do
prerender_commit_full_titles!(commits)
end
end end
end end
private private
def more_url(offset)
logs_file_project_ref_path(@project, @ref, @path, offset: offset)
end
def prerender_commit_full_titles!(commits)
# Preload commit authors as they are used in rendering
commits.each(&:lazy_author)
renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project)
renderer.render(commits, :full_title)
end
def validate_ref_id def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end end
......
class TemplateFinder
prepend ::EE::TemplateFinder
VENDORED_TEMPLATES = {
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate
}.freeze
class << self
def build(type, params = {})
if type == :licenses
LicenseTemplateFinder.new(params)
else
new(type, params)
end
end
end
attr_reader :type, :params
attr_reader :vendored_templates
private :vendored_templates
def initialize(type, params = {})
@type = type
@params = params
@vendored_templates = VENDORED_TEMPLATES.fetch(type)
end
def execute
if params[:name]
vendored_templates.find(params[:name])
else
vendored_templates.all
end
end
end
...@@ -158,32 +158,35 @@ module BlobHelper ...@@ -158,32 +158,35 @@ module BlobHelper
end end
def licenses_for_select def licenses_for_select
return @licenses_for_select if defined?(@licenses_for_select) @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute)
grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category)
categories = grouped_licenses.keys
@licenses_for_select = categories.each_with_object({}) do |category, hash|
hash[category] = grouped_licenses[category].map do |license|
{ name: license.name, id: license.id }
end
end
end end
def ref_project def ref_project
@ref_project ||= @target_project || @project @ref_project ||= @target_project || @project
end end
def template_dropdown_names(items)
grouped = items.group_by(&:category)
categories = grouped.keys
categories.each_with_object({}) do |category, hash|
hash[category] = grouped[category].map do |item|
{ name: item.name, id: item.id }
end
end
end
private :template_dropdown_names
def gitignore_names def gitignore_names
@gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute)
end end
def gitlab_ci_ymls def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls).execute)
end end
def dockerfile_names def dockerfile_names
@dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute)
end end
def blob_editor_paths def blob_editor_paths
......
...@@ -60,9 +60,8 @@ module ButtonHelper ...@@ -60,9 +60,8 @@ module ButtonHelper
protocol = gitlab_config.protocol.upcase protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol) dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link append_url = project.http_url_to_repo if append_link
geo_url = geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?
dropdown_item_with_description(protocol, dropdown_description, href: append_url, geo_url: geo_url) dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' })
end end
def http_dropdown_description(protocol) def http_dropdown_description(protocol)
...@@ -80,12 +79,11 @@ module ButtonHelper ...@@ -80,12 +79,11 @@ module ButtonHelper
end end
append_url = project.ssh_url_to_repo if append_link append_url = project.ssh_url_to_repo if append_link
geo_url = geo_primary_ssh_url_to_repo(project) if Gitlab::Geo.secondary?
dropdown_item_with_description('SSH', dropdown_description, href: append_url, geo_url: geo_url) dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end end
def dropdown_item_with_description(title, description, href: nil, geo_url: nil) def dropdown_item_with_description(title, description, href: nil, data: nil)
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
...@@ -93,9 +91,7 @@ module ButtonHelper ...@@ -93,9 +91,7 @@ module ButtonHelper
(href ? button_content : title), (href ? button_content : title),
class: "#{title.downcase}-selector", class: "#{title.downcase}-selector",
href: (href if href), href: (href if href),
data: { data: (data if data)
primary_url: (geo_url if geo_url)
}
end end
def kerberos_clone_button(project) def kerberos_clone_button(project)
......
...@@ -11,4 +11,8 @@ module ClustersHelper ...@@ -11,4 +11,8 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner' render 'projects/clusters/gcp_signup_offer_banner'
end end
end end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end end
...@@ -86,7 +86,7 @@ module IconsHelper ...@@ -86,7 +86,7 @@ module IconsHelper
end end
end end
def visibility_level_icon(level, fw: true) def visibility_level_icon(level, fw: true, options: {})
name = name =
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
...@@ -99,7 +99,7 @@ module IconsHelper ...@@ -99,7 +99,7 @@ module IconsHelper
name << " fw" if fw name << " fw" if fw
icon(name) icon(name, options)
end end
def file_type_icon_class(type, mode, name) def file_type_icon_class(type, mode, name)
......
...@@ -107,23 +107,23 @@ module MarkupHelper ...@@ -107,23 +107,23 @@ module MarkupHelper
def markup(file_name, text, context = {}) def markup(file_name, text, context = {})
context[:project] ||= @project context[:project] ||= @project
context[:markdown_engine] ||= :redcarpet context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html = context.delete(:rendered) || markup_unsafe(file_name, text, context) html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
def render_wiki_content(wiki_page) def render_wiki_content(wiki_page, context = {})
text = wiki_page.content text = wiki_page.content
return '' unless text.present? return '' unless text.present?
context = { context.merge!(
pipeline: :wiki, pipeline: :wiki,
project: @project, project: @project,
project_wiki: @project_wiki, project_wiki: @project_wiki,
page_slug: wiki_page.slug, page_slug: wiki_page.slug,
issuable_state_filter_enabled: true, issuable_state_filter_enabled: true
markdown_engine: :redcarpet )
} context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html = html =
case wiki_page.format case wiki_page.format
...@@ -178,6 +178,10 @@ module MarkupHelper ...@@ -178,6 +178,10 @@ module MarkupHelper
end end
end end
def commonmark_for_repositories_enabled?
Feature.enabled?(:commonmark_for_repositories, default_enabled: true)
end
private private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML # Return +text+, truncated to +max_chars+ characters, excluding any HTML
......
...@@ -254,6 +254,10 @@ module ProjectsHelper ...@@ -254,6 +254,10 @@ module ProjectsHelper
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end end
def legacy_render_context(params)
params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
end
private private
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
...@@ -353,6 +357,10 @@ module ProjectsHelper ...@@ -353,6 +357,10 @@ module ProjectsHelper
end end
end end
def default_clone_label
_("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase }
end
def default_clone_protocol def default_clone_protocol
if allowed_protocols_present? if allowed_protocols_present?
enabled_protocol enabled_protocol
......
module UserCalloutsHelper module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze
def show_gke_cluster_integration_callout?(project) def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) && can?(current_user, :create_cluster, project) &&
...@@ -11,6 +12,10 @@ module UserCalloutsHelper ...@@ -11,6 +12,10 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER) !user_dismissed?(GCP_SIGNUP_OFFER)
end end
def show_cluster_security_warning?
!user_dismissed?(CLUSTER_SECURITY_WARNING)
end
private private
def user_dismissed?(feature_name) def user_dismissed?(feature_name)
......
...@@ -138,7 +138,7 @@ module VisibilityLevelHelper ...@@ -138,7 +138,7 @@ module VisibilityLevelHelper
end end
def project_visibility_icon_description(level) def project_visibility_icon_description(level)
"#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" "#{project_visibility_level_description(level)}"
end end
def visibility_level_label(level) def visibility_level_label(level)
......
# frozen_string_literal: true
module Emails
module AutoDevops
def autodevops_disabled_email(pipeline, recipient)
@pipeline = pipeline
@project = pipeline.project
add_project_headers
mail(to: recipient,
subject: auto_devops_disabled_subject(@project.name)) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
end
private
def auto_devops_disabled_subject(project_name)
subject("Auto DevOps pipeline was disabled for #{project_name}")
end
end
end
...@@ -14,6 +14,7 @@ class Notify < BaseMailer ...@@ -14,6 +14,7 @@ class Notify < BaseMailer
include Emails::Profile include Emails::Profile
include Emails::Pipelines include Emails::Pipelines
include Emails::Members include Emails::Members
include Emails::AutoDevops
helper MergeRequestsHelper helper MergeRequestsHelper
helper DiffHelper helper DiffHelper
......
...@@ -127,6 +127,10 @@ class NotifyPreview < ActionMailer::Preview ...@@ -127,6 +127,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end end
def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message
end
private private
def project def project
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator class Blob < SimpleDelegator
prepend EE::Blob
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
......
...@@ -90,7 +90,9 @@ module Ci ...@@ -90,7 +90,9 @@ module Ci
end end
def hashed_path? def hashed_path?
super || self.try(:file_location).nil? return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column
super || self.file_location.nil?
end end
def expire_in def expire_in
......
...@@ -174,6 +174,12 @@ module Ci ...@@ -174,6 +174,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id) PipelineNotificationWorker.perform_async(pipeline.id)
end end
end end
after_transition any => [:failed] do |pipeline|
next unless pipeline.auto_devops_source?
pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
end
end end
scope :internal, -> { where(source: internal_sources) } scope :internal, -> { where(source: internal_sources) }
......
...@@ -32,7 +32,8 @@ module Clusters ...@@ -32,7 +32,8 @@ module Clusters
def install_command def install_command
Gitlab::Kubernetes::Helm::InitCommand.new( Gitlab::Kubernetes::Helm::InitCommand.new(
name: name, name: name,
files: files files: files,
rbac: cluster.platform_kubernetes_rbac?
) )
end end
......
...@@ -39,6 +39,7 @@ module Clusters ...@@ -39,6 +39,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
version: VERSION, version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart, chart: chart,
files: files files: files
) )
......
...@@ -40,6 +40,7 @@ module Clusters ...@@ -40,6 +40,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
version: VERSION, version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart, chart: chart,
files: files, files: files,
repository: repository repository: repository
......
...@@ -50,6 +50,7 @@ module Clusters ...@@ -50,6 +50,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
version: VERSION, version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart, chart: chart,
files: files files: files
) )
...@@ -73,7 +74,7 @@ module Clusters ...@@ -73,7 +74,7 @@ module Clusters
private private
def kube_client def kube_client
cluster&.kubeclient cluster&.kubeclient&.core_client
end end
end end
end end
......
...@@ -33,6 +33,7 @@ module Clusters ...@@ -33,6 +33,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new( Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
version: VERSION, version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart, chart: chart,
files: files, files: files,
repository: repository repository: repository
......
...@@ -44,6 +44,7 @@ module Clusters ...@@ -44,6 +44,7 @@ module Clusters
delegate :on_creation?, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
......
...@@ -5,6 +5,7 @@ module Clusters ...@@ -5,6 +5,7 @@ module Clusters
class Kubernetes < ActiveRecord::Base class Kubernetes < ActiveRecord::Base
include Gitlab::Kubernetes include Gitlab::Kubernetes
include ReactiveCaching include ReactiveCaching
include EnumWithNil
prepend EE::KubernetesService prepend EE::KubernetesService
...@@ -49,6 +50,12 @@ module Clusters ...@@ -49,6 +50,12 @@ module Clusters
alias_method :active?, :enabled? alias_method :active?, :enabled?
enum_with_nil authorization_type: {
unknown_authorization: nil,
rbac: 1,
abac: 2
}
def actual_namespace def actual_namespace
if namespace.present? if namespace.present?
namespace namespace
...@@ -97,7 +104,7 @@ module Clusters ...@@ -97,7 +104,7 @@ module Clusters
end end
def kubeclient def kubeclient
@kubeclient ||= build_kubeclient! @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end end
private private
...@@ -117,15 +124,16 @@ module Clusters ...@@ -117,15 +124,16 @@ module Clusters
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace raise "Incomplete settings" unless api_url && actual_namespace
unless (username && password) || token unless (username && password) || token
raise "Either username/password or token is required to access API" raise "Either username/password or token is required to access API"
end end
::Kubeclient::Client.new( Gitlab::Kubernetes::KubeClient.new(
join_api_url(api_path), api_url,
api_groups,
api_version, api_version,
auth_options: kubeclient_auth_options, auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options, ssl_options: kubeclient_ssl_options,
...@@ -135,7 +143,7 @@ module Clusters ...@@ -135,7 +143,7 @@ module Clusters
# Returns a hash of all pods in the namespace # Returns a hash of all pods in the namespace
def read_pods def read_pods
kubeclient = build_kubeclient! kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err rescue Kubeclient::HttpError => err
...@@ -159,15 +167,6 @@ module Clusters ...@@ -159,15 +167,6 @@ module Clusters
{ bearer_token: token } { bearer_token: token }
end end
def join_api_url(api_path)
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
url.path = [prefix, api_path].join("/")
url.to_s
end
def terminal_auth def terminal_auth
{ {
token: token, token: token,
......
...@@ -22,6 +22,7 @@ class Commit ...@@ -22,6 +22,7 @@ class Commit
attr_accessor :project, :author attr_accessor :project, :author
attr_accessor :redacted_description_html attr_accessor :redacted_description_html
attr_accessor :redacted_title_html attr_accessor :redacted_title_html
attr_accessor :redacted_full_title_html
attr_reader :gpg_commit attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
......
...@@ -9,23 +9,46 @@ module CaseSensitivity ...@@ -9,23 +9,46 @@ module CaseSensitivity
# #
# Unlike other ActiveRecord methods this method only operates on a Hash. # Unlike other ActiveRecord methods this method only operates on a Hash.
def iwhere(params) def iwhere(params)
criteria = self criteria = self
cast_lower = Gitlab::Database.postgresql?
params.each do |key, value| params.each do |key, value|
column = ActiveRecord::Base.connection.quote_table_name(key) criteria = case value
when Array
criteria.where(value_in(key, value))
else
criteria.where(value_equal(key, value))
end
end
criteria
end
condition = private
if cast_lower
"LOWER(#{column}) = LOWER(:value)" def value_equal(column, value)
else lower_value = lower_value(value)
"#{column} = :value"
end lower_column(arel_table[column]).eq(lower_value).to_sql
end
criteria = criteria.where(condition, value: value) def value_in(column, values)
lower_values = values.map do |value|
lower_value(value)
end end
criteria lower_column(arel_table[column]).in(lower_values).to_sql
end
def lower_value(value)
return value if Gitlab::Database.mysql?
Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)])
end
def lower_column(column)
return column if Gitlab::Database.mysql?
column.lower
end end
end end
end end
...@@ -98,10 +98,10 @@ class KubernetesService < DeploymentService ...@@ -98,10 +98,10 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API # Check we can connect to the Kubernetes API
def test(*args) def test(*args)
kubeclient = build_kubeclient! kubeclient = build_kube_client!
kubeclient.discover kubeclient.core_client.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" } { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" }
rescue => err rescue => err
{ success: false, result: err } { success: false, result: err }
end end
...@@ -146,7 +146,7 @@ class KubernetesService < DeploymentService ...@@ -146,7 +146,7 @@ class KubernetesService < DeploymentService
end end
def kubeclient def kubeclient
@kubeclient ||= build_kubeclient! @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end end
def deprecated? def deprecated?
...@@ -184,11 +184,12 @@ class KubernetesService < DeploymentService ...@@ -184,11 +184,12 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace && token raise "Incomplete settings" unless api_url && actual_namespace && token
::Kubeclient::Client.new( Gitlab::Kubernetes::KubeClient.new(
join_api_url(api_path), api_url,
api_groups,
api_version, api_version,
auth_options: kubeclient_auth_options, auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options, ssl_options: kubeclient_ssl_options,
...@@ -198,7 +199,7 @@ class KubernetesService < DeploymentService ...@@ -198,7 +199,7 @@ class KubernetesService < DeploymentService
# Returns a hash of all pods in the namespace # Returns a hash of all pods in the namespace
def read_pods def read_pods
kubeclient = build_kubeclient! kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err rescue Kubeclient::HttpError => err
...@@ -222,15 +223,6 @@ class KubernetesService < DeploymentService ...@@ -222,15 +223,6 @@ class KubernetesService < DeploymentService
{ bearer_token: token } { bearer_token: token }
end end
def join_api_url(api_path)
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
url.path = [prefix, api_path].join("/")
url.to_s
end
def terminal_auth def terminal_auth
{ {
token: token, token: token,
......
...@@ -585,7 +585,12 @@ class Repository ...@@ -585,7 +585,12 @@ class Repository
end end
def rendered_readme def rendered_readme
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme return unless readme
context = { project: project }
context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled?
MarkupHelper.markup_unsafe(readme.name, readme.data, context)
end end
cache_method :rendered_readme cache_method :rendered_readme
......
...@@ -266,6 +266,7 @@ class User < ActiveRecord::Base ...@@ -266,6 +266,7 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :by_username, -> (usernames) { iwhere(username: usernames) }
# Limits the users to those that have TODOs, optionally in the given state. # Limits the users to those that have TODOs, optionally in the given state.
# #
...@@ -457,11 +458,11 @@ class User < ActiveRecord::Base ...@@ -457,11 +458,11 @@ class User < ActiveRecord::Base
end end
def find_by_username(username) def find_by_username(username)
iwhere(username: username).take by_username(username).take
end end
def find_by_username!(username) def find_by_username!(username)
iwhere(username: username).take! by_username(username).take!
end end
def find_by_personal_access_token(token_string) def find_by_personal_access_token(token_string)
......
...@@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base ...@@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: { enum feature_name: {
gke_cluster_integration: 1, gke_cluster_integration: 1,
gcp_signup_offer: 2 gcp_signup_offer: 2,
cluster_security_warning: 3
} }
validates :user, presence: true validates :user, presence: true
......
This diff is collapsed.
...@@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity ...@@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity
erase_project_job_path(project, build) erase_project_job_path(project, build)
end end
expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build|
terminal_project_job_path(project, build)
end
expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do
expose :iid do |build| expose :iid do |build|
build.merge_request.iid build.merge_request.iid
...@@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity ...@@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity
def project def project
build.project build.project
end end
def can_create_build_terminal?
can?(current_user, :create_build_terminal, build) && build.has_terminal?
end
end end
...@@ -411,6 +411,12 @@ class NotificationService ...@@ -411,6 +411,12 @@ class NotificationService
end end
end end
def autodevops_disabled(pipeline, recipients)
recipients.each do |recipient|
mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
end
end
def pages_domain_verification_succeeded(domain) def pages_domain_verification_succeeded(domain)
recipients_for_pages_domain(domain).each do |user| recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
......
...@@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService ...@@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService
end end
def markdown_engine def markdown_engine
CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) if params[:legacy_render]
:redcarpet
else
CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
end
end end
end end
# frozen_string_literal: true
module Projects
module AutoDevops
class DisableService < BaseService
def execute
return false unless implicitly_enabled_and_first_pipeline_failure?
disable_auto_devops
end
private
def implicitly_enabled_and_first_pipeline_failure?
project.has_auto_devops_implicitly_enabled? &&
first_pipeline_failure?
end
# We're using `limit` to optimize `auto_devops pipeline` query,
# since we only care about the first element, and using only `.count`
# is an expensive operation. See
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378
# for more context.
def first_pipeline_failure?
auto_devops_pipelines.success.limit(1).count.zero? &&
auto_devops_pipelines.failed.limit(1).count.nonzero?
end
def disable_auto_devops
project.auto_devops_attributes = { enabled: false }
project.save!
end
def auto_devops_pipelines
@auto_devops_pipelines ||= project.pipelines.auto_devops_source
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= form_errors(@application_setting) = form_errors(@application_setting)
%fieldset %fieldset
.form-group .form-group.mb-2
.form-check .form-check
= f.check_box :version_check_enabled, class: 'form-check-input' = f.check_box :version_check_enabled, class: 'form-check-input'
= f.label :version_check_enabled, class: 'form-check-label' do = f.label :version_check_enabled, class: 'form-check-label' do
...@@ -16,23 +16,26 @@ ...@@ -16,23 +16,26 @@
.form-check .form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do = f.label :usage_ping_enabled, class: 'form-check-label' do
Enable usage ping = _('Enable usage ping')
.form-text.text-muted .form-text.text-muted
- if can_be_configured - if can_be_configured
To help improve GitLab and its user experience, GitLab will %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
periodically collect usage information.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
about what information is shared with GitLab Inc. Visit - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping') %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
to see the JSON payload sent.
%button.btn.js-usage-ping-payload-trigger{ type: 'button' }
.js-spinner.d-none= icon('spinner spin')
.js-text.d-inline= _('Preview payload')
%pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else - else
The usage ping is disabled, and cannot be configured through this = _('The usage ping is disabled, and cannot be configured through this form.')
form. For more information, see the documentation on - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
= succeed '.' do - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
.form-group .form-group.mt-3
= f.label :instance_statistics_visibility_private, _('Instance Statistics visibility') = f.label :instance_statistics_visibility_private, _('Instance Statistics visibility')
= f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control' = f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group .form-group
= f.label "Username or email", for: "user_login" = f.label "Username or email", for: "user_login", class: 'label-bold'
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
.form-group .form-group
= f.label :password = f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me .remember-me
......
.omniauth-container .omniauth-container.prepend-top-15
%p %label.label-bold.d-block
%span.light Sign in with
Sign in with &nbsp; - providers = enabled_button_based_providers
- providers = enabled_button_based_providers .d-flex.justify-content-between.flex-wrap
- providers.each do |provider| - providers.each do |provider|
%span.light - has_icon = provider_has_icon?(provider)
- has_icon = provider_has_icon?(provider) = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - if has_icon
%fieldset.prepend-top-10.remember-me = provider_image_tag(provider)
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
%span %span
Remember me = label_for_provider(provider)
%fieldset.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
%span
Remember me
...@@ -4,24 +4,24 @@ ...@@ -4,24 +4,24 @@
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.form-group .form-group
= f.label :name, 'Full name' = f.label :name, 'Full name', class: 'label-bold'
= f.text_field :name, class: "form-control top", required: true, title: "This field is required." = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group .username.form-group
= f.label :username = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken. %p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available. %p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability... %p.validation-pending.hide Checking username availability...
.form-group .form-group
= f.label :email = f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
.form-group .form-group
= f.label :email_confirmation = f.label :email_confirmation, class: 'label-bold'
= f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address."
.form-group.append-bottom-20#password-strength .form-group.append-bottom-20#password-strength
= f.label :password = f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters
- if Gitlab::CurrentSettings.current_application_settings.enforce_terms? - if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
.form-group .form-group
= check_box_tag :terms_opt_in, '1', false, required: true = check_box_tag :terms_opt_in, '1', false, required: true
...@@ -35,8 +35,3 @@ ...@@ -35,8 +35,3 @@
= recaptcha_tags = recaptcha_tags
.submit-container .submit-container
= f.submit "Register", class: "btn-register btn" = f.submit "Register", class: "btn-register btn"
.clearfix.submit-container
%p
%span.light Didn't receive a confirmation email?
= succeed '.' do
= link_to "Request a new one", new_confirmation_path(:user)
- breadcrumb_title "Cohorts" - breadcrumb_title _("Cohorts")
- @no_container = true - @no_container = true
%div{ class: container_class } %div{ class: container_class }
- if @cohorts - if @cohorts
= render 'cohorts_table' = render 'cohorts_table'
= render 'usage_ping'
- else - else
.bs-callout.bs-callout-warning.clearfix .bs-callout.bs-callout-warning.clearfix
%p %p
User cohorts are only shown when the - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
= link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank' - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
is enabled. To enable it and see user cohorts, = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
visit - if current_user.admin?
= succeed '.' do - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics')
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics') - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path }
= s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe }
.container.convdev-empty .container.convdev-empty
.col-sm-12.justify-content-center.text-center .col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index') = custom_icon('convdev_no_index')
%h4 Usage ping is not enabled %h4= _('Usage ping is not enabled')
%p - if !current_user.admin?
ConvDev is only shown when the %p
= link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- if current_user.admin?
%p
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- if current_user.admin?
= link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
- @no_container = true - @no_container = true
- page_title 'ConvDev Index' - page_title _('ConvDev Index')
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container .container
- if show_callout?('convdev_intro_callout_dismissed') - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed')
= render 'callout' = render 'callout'
.prepend-top-default .prepend-top-default
- if !Gitlab::CurrentSettings.usage_ping_enabled - if !usage_ping_enabled
= render 'disabled' = render 'disabled'
- elsif @metric.blank? - elsif @metric.blank?
= render 'no_data' = render 'no_data'
......
...@@ -18,16 +18,17 @@ ...@@ -18,16 +18,17 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('ConvDev Index') = _('ConvDev Index')
= nav_link(controller: :cohorts) do - if Gitlab::CurrentSettings.usage_ping_enabled
= link_to instance_statistics_cohorts_path do = nav_link(controller: :cohorts) do
.nav-icon-container = link_to instance_statistics_cohorts_path do
= sprite_icon('users') .nav-icon-container
%span.nav-item-name = sprite_icon('users')
= _('Cohorts') %span.nav-item-name
%ul.sidebar-sub-level-items.is-fly-out-only = _('Cohorts')
= nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do %ul.sidebar-sub-level-items.is-fly-out-only
= link_to instance_statistics_cohorts_path do = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
%strong.fly-out-top-item-name = link_to instance_statistics_cohorts_path do
= _('Cohorts') %strong.fly-out-top-item-name
= _('Cohorts')
= render 'shared/sidebar_toggle_button' = render 'shared/sidebar_toggle_button'
%tr
%td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" }
had
= failed.size
failed
#{'build'.pluralize(failed.size)}.
%tr.table-warning
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" }
%tbody
%tr
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" }
%img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" }
= build.stage
%td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build
%tr.build-log
- if build.has_trace?
%td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" }
%pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" }
= build.trace.html(last_lines: 10).html_safe
- else
%td{ colspan: "2" }
%tr.alert
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" }
Auto DevOps pipeline was disabled for #{@project.name}
%tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" }
The Auto DevOps pipeline failed for pipeline
%a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" }
= "\##{@pipeline.iid}"
and has been disabled for
%a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" }
= @project.name + "."
In order to use the Auto DevOps pipeline with your project, please review the
%a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages,
adjust your project accordingly, and turn on the Auto DevOps pipeline within your
%a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" }
CI/CD project settings.
%tr.pre-section
%td{ style: 'text-align: center;border-bottom:1px solid #ededed' }
%a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' }
Learn more about Auto DevOps
%tr.pre-section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" }
Pipeline
%a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" }
= "\##{@pipeline.id}"
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" }
= @pipeline.user.name
- else
%td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
API
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
<% failed = @pipeline.statuses.latest.failed -%>
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
<% if build.has_trace? -%>
Trace: <%= build.trace.raw(last_lines: 10) %>
<% end -%>
<% end -%>
...@@ -107,36 +107,5 @@ ...@@ -107,36 +107,5 @@
- else - else
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
API API
- failed = @pipeline.statuses.latest.failed
%tr = render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
%td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
had
= failed.size
failed
#{'build'.pluralize(failed.size)}.
%tr.table-warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
%img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage
%td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
%tr.build-log
- if build.has_trace?
%td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
%pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
= build.trace.html(last_lines: 10).html_safe
- else
%td{ colspan: "2" }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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