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:
- error
- max: 1
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:
- error
- 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'
gem 'validates_hostname', '~> 1.0.6'
# Browser detection
gem 'browser', '~> 2.2'
gem 'browser', '~> 2.5'
# GPG
gem 'gpgme'
......@@ -110,7 +110,9 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.8.8'
# 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'
# Drag and Drop UI
......
......@@ -98,7 +98,7 @@ GEM
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
browser (2.5.3)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
......@@ -1021,12 +1021,12 @@ DEPENDENCIES
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
browser (~> 2.5)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
capybara (~> 2.15)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2)
carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......
......@@ -101,7 +101,7 @@ GEM
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
browser (2.5.3)
builder (3.2.3)
bullet (5.5.1)
activesupport (>= 3.0.0)
......@@ -1030,12 +1030,12 @@ DEPENDENCIES
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
browser (~> 2.5)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
capybara (~> 2.15)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.2)
carrierwave (= 1.2.3)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......
......@@ -33,7 +33,7 @@ Documentation:
http://doc.gitlab.com/ee/
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/)
......
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 Vue from 'vue';
import initDismissableCallout from '~/dismissable_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from './constants';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
......@@ -66,6 +62,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
......@@ -129,7 +126,8 @@ export default class Clusters {
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData()
this.service
.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
......@@ -177,15 +175,21 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null)
.filter(
appId =>
newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null,
)
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
appList: appTitles.join(', '),
});
const text = sprintf(
s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
{
appList: appTitles.join(', '),
},
);
Flash(text, 'notice', this.successApplicationContainer);
}
}
......@@ -218,13 +222,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId, data.params)
this.service
.installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
.catch(() => {
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 { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer();
initDismissableCallout('.gcp-signup-offer');
// The empty state won't have a clusterList
if (clusterList) {
......
......@@ -63,7 +63,7 @@ export default {
v-else
role="button"
class="fa fa-times dropdown-input-search"
@click="clearSearch"
@click.stop.prevent="clearSearch"
></i>
</div>
<div class="dropdown-content">
......
......@@ -107,7 +107,6 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
// eslint-disable-next-line no-param-reassign
state.diffFiles = state.diffFiles.map(file => ({
...file,
collapsed: false,
......
......@@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function gcpSignupOffer() {
const alertEl = document.querySelector('.gcp-signup-offer');
export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector(alertSelector);
if (!alertEl) {
return;
}
......
......@@ -65,8 +65,8 @@ export const hideMenu = (el) => {
const parentEl = el.parentNode;
el.style.display = ''; // eslint-disable-line no-param-reassign
el.style.transform = ''; // eslint-disable-line no-param-reassign
el.style.display = '';
el.style.transform = '';
el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import { normalizeJob } from './utils';
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
......
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
import { sortTree } from '../utils';
import { diffModes } from '../../constants';
......
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
......
import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
import UsagePingPayload from './usage_ping_payload';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
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 Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
......@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
gcpSignupOffer();
initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns();
}
......
......@@ -13,45 +13,57 @@ export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
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) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
$('a', $cloneOptions).on('click', (e) => {
$('a', $cloneOptions).on('click', e => {
e.preventDefault();
const $this = $(e.currentTarget);
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');
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text(activeText);
$el.toggleClass('is-active');
$label.text(activeText);
});
$('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'),
cloneUrlPrimary: $this.data('primaryUrl') || '',
});
return $('.clone').text(url);
$projectCloneField.val(url);
$('.js-git-empty .js-clone').text(url);
});
// Ref switcher
Project.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
return $(this)
.parents('form')
.submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
$(this).parents('.no-ssh-key-message').remove();
$(this)
.parents('.no-ssh-key-message')
.remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
Cookies.set('hide_no_password_message', 'false');
$(this).parents('.no-password-message').remove();
$(this)
.parents('.no-password-message')
.remove();
return e.preventDefault();
});
$('.hide-shared-runner-limit-message').on('click', function(e) {
......@@ -70,7 +82,7 @@ export default class Project {
}
static changeProject(url) {
return window.location = url;
return (window.location = url);
}
static initRefSwitcher() {
......@@ -85,14 +97,15 @@ export default class Project {
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data(term, callback) {
axios.get($dropdown.data('refsUrl'), {
params: {
ref: $dropdown.data('ref'),
search: term,
},
})
.then(({ data }) => callback(data))
.catch(() => flash(__('An error occurred while getting projects')));
axios
.get($dropdown.data('refsUrl'), {
params: {
ref: $dropdown.data('ref'),
search: term,
},
})
.then(({ data }) => callback(data))
.catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
......
......@@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
import initReadMore from '~/read_more';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => {
initReadMore();
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // 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,
className: 'js-autodevops-banner',
});
......
......@@ -132,10 +132,8 @@ export default {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
// eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
// eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
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';
export default {
......
......@@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => {
response.headers.forEach((value, key) => {
headers[key] = value;
});
// eslint-disable-next-line no-param-reassign
response.headers = headers;
});
});
......@@ -66,3 +66,4 @@
@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
@import 'framework/terms';
@import 'framework/read_more';
......@@ -229,8 +229,8 @@
svg {
margin-bottom: 1px;
height: 18px;
width: 18px;
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
path {
......
......@@ -149,7 +149,8 @@
&.btn-success,
&.btn-new,
&.btn-create,
&.btn-save {
&.btn-save,
&.btn-register {
@include btn-green;
}
......@@ -172,8 +173,7 @@
}
&.btn-info,
&.btn-primary,
&.btn-register {
&.btn-primary {
@include btn-blue;
}
......@@ -248,7 +248,7 @@
.btn-terminal {
svg {
height: 14px;
width: 18px;
width: $default-icon-size;
}
}
......
......@@ -216,8 +216,8 @@
vertical-align: inherit;
img {
height: 18px;
width: 18px;
height: $default-icon-size;
width: $default-icon-size;
}
}
......
......@@ -44,12 +44,8 @@
.project-repo-buttons {
display: block;
.count-buttons .btn {
margin: 0 10px;
}
.count-buttons .count-with-arrow {
display: none;
.count-buttons .count-badge {
margin-top: $gl-padding-8;
}
}
}
......
.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 @@
&,
.toggle-icon-svg {
width: 18px;
height: 18px;
width: $default-icon-size;
height: $default-icon-size;
}
.toggle-icon-svg {
......
......@@ -252,7 +252,7 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px;
$border-radius-default: 4px;
$border-radius-small: 2px;
$settings-icon-size: 18px;
$default-icon-size: 18px;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
......@@ -274,6 +274,7 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$project-title-row-height: 24px;
// EE-only CSS variables START
$system-header-height: 35px;
......
......@@ -4,3 +4,7 @@
padding-bottom: 46px;
}
}
.usage-data {
max-height: 400px;
}
......@@ -749,6 +749,10 @@
left: $gl-padding;
}
.dropdown-input .dropdown-input-search {
pointer-events: all;
}
.diff-changed-file {
display: flex;
padding-top: 8px;
......
......@@ -100,6 +100,22 @@
p {
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 {
......@@ -169,10 +185,6 @@
}
}
label {
font-weight: $gl-font-weight-normal;
}
.submit-container {
margin-top: 16px;
}
......@@ -200,15 +212,6 @@
}
}
.oauth-image-link {
margin-right: 10px;
img {
width: 32px;
height: 32px;
}
}
.devise-layout-html {
margin: 0;
padding: 0;
......
......@@ -115,7 +115,7 @@
.project-feature-controls {
display: flex;
align-items: center;
margin: 8px 0;
margin: $gl-padding-8 0;
max-width: 432px;
.toggle-wrapper {
......@@ -144,12 +144,8 @@
.group-home-panel {
padding-top: 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 {
float: none;
margin: 0 auto;
......@@ -175,7 +171,6 @@
}
}
.project-home-desc,
.group-home-desc {
margin-left: auto;
margin-right: auto;
......@@ -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 {
margin-top: 0;
}
......@@ -206,8 +257,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
padding: 3px 10px;
&:last-child {
margin-left: 0;
}
......@@ -222,11 +271,15 @@
.fa-caret-down {
margin-left: 3px;
&.dropdown-btn-icon {
margin-left: 0;
}
}
}
.project-action-button {
margin: 15px 5px 0;
margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top;
}
......@@ -243,82 +296,45 @@
.count-buttons {
display: inline-block;
vertical-align: top;
margin-top: 15px;
}
margin-top: $gl-padding;
.project-clone-holder {
display: inline-block;
margin: 15px 5px 0 0;
.count-badge {
height: $input-height;
input {
height: 28px;
.icon {
top: -1px;
}
}
}
.count-with-arrow {
display: inline-block;
position: relative;
margin-left: 4px;
.count-badge-count,
.count-badge-button {
border: 1px solid $border-color;
line-height: 1;
}
.arrow {
&::before {
content: '';
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;
}
.count,
.count-badge-button {
color: $gl-text-color;
}
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
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-badge-count {
padding: 0 12px;
border-right: 0;
border-radius: $border-radius-base 0 0 $border-radius-base;
background: $gray-light;
}
.count {
@include btn-white;
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;
.count-badge-button {
border-radius: 0 $border-radius-base $border-radius-base 0;
}
}
a {
color: inherit;
}
.project-clone-holder {
display: inline-block;
margin: $gl-padding $gl-padding-8 0 0;
&:hover {
background: $white-light;
}
input {
height: $input-height;
}
}
......@@ -333,6 +349,14 @@
min-width: 320px;
}
}
.mobile-git-clone {
margin-top: $gl-padding-8;
.dropdown-menu-inner-content {
@extend .monospace;
}
}
}
.split-one {
......@@ -512,7 +536,6 @@
.controls {
margin-left: auto;
}
}
.choose-template {
......@@ -575,7 +598,7 @@
flex-wrap: wrap;
.btn {
padding: 8px;
padding: $gl-padding-8;
margin-right: 10px;
}
......@@ -652,7 +675,7 @@
left: -10px;
top: 50%;
z-index: 10;
padding: 8px 0;
padding: $gl-padding-8 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
......@@ -666,7 +689,7 @@
left: 50%;
top: 0;
transform: translateX(-50%);
padding: 0 8px;
padding: 0 $gl-padding-8;
}
}
......@@ -700,17 +723,51 @@
.project-stats {
font-size: 0;
text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color;
.nav {
margin-top: $gl-padding-8;
margin-bottom: $gl-padding-8;
.scrolling-tabs-container {
.scrolling-tabs {
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 {
display: inline-block;
margin-top: $gl-padding-4;
margin-bottom: $gl-padding-4;
&:not(:last-child) {
margin-right: $gl-padding;
......@@ -733,13 +790,17 @@
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
color: $gl-text-color-secondary;
white-space: nowrap;
}
.stat-link {
border-bottom: 0;
&:hover,
&:focus {
color: $gl-text-color;
text-decoration: underline;
border-bottom: 0;
}
}
......@@ -869,7 +930,7 @@ pre.light-well {
}
.git-clone-holder {
width: 380px;
width: 320px;
.btn-clipboard {
border: 1px solid $border-color;
......
......@@ -106,7 +106,7 @@
.settings-list-icon {
color: $gl-text-color-secondary;
font-size: $settings-icon-size;
font-size: $default-icon-size;
line-height: 42px;
}
......
......@@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base
def append_info_to_payload(payload)
super
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
logged_user = auth_user
......
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: '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
# content-type of text/javascript, which will trigger Rails'
# cross-origin JavaScript protection.
......@@ -18,4 +22,14 @@ module SendFileUpload
redirect_to file_upload.url(**redirect_params)
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
# frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
before_action :authenticate_usage_ping_enabled_or_admin!
def index
if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
......@@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
def authenticate_usage_ping_enabled_or_admin!
render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
end
end
......@@ -159,7 +159,8 @@ class Projects::ClustersController < Projects::ApplicationController
:namespace,
:api_url,
:token,
:ca_cert
:ca_cert,
:authorization_type
]).merge(
provider_type: :user,
platform_type: :kubernetes
......
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
include PathLocksHelper
before_action :require_non_empty_project
before_action :validate_ref_id
......@@ -37,58 +36,47 @@ class Projects::RefsController < Projects::ApplicationController
end
def logs_tree
@offset = if params[:offset].present?
params[:offset].to_i
else
0
end
summary = ::Gitlab::TreeSummary.new(
@commit,
@project,
path: @path,
offset: params[:offset],
limit: 25
)
@limit = 25
@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
@logs, commits = summary.summarize
@more_log_url = more_url(summary.next_offset) if summary.more?
respond_to do |format|
format.html { render_404 }
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
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
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
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
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
end
def licenses_for_select
return @licenses_for_select if defined?(@licenses_for_select)
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
@licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute)
end
def ref_project
@ref_project ||= @target_project || @project
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
@gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
@gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute)
end
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
def dockerfile_names
@dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
@dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute)
end
def blob_editor_paths
......
......@@ -60,9 +60,8 @@ module ButtonHelper
protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol)
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
def http_dropdown_description(protocol)
......@@ -80,12 +79,11 @@ module ButtonHelper
end
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
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(:span, description, class: 'dropdown-menu-inner-content') if description
......@@ -93,9 +91,7 @@ module ButtonHelper
(href ? button_content : title),
class: "#{title.downcase}-selector",
href: (href if href),
data: {
primary_url: (geo_url if geo_url)
}
data: (data if data)
end
def kerberos_clone_button(project)
......
......@@ -11,4 +11,8 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner'
end
end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end
......@@ -86,7 +86,7 @@ module IconsHelper
end
end
def visibility_level_icon(level, fw: true)
def visibility_level_icon(level, fw: true, options: {})
name =
case level
when Gitlab::VisibilityLevel::PRIVATE
......@@ -99,7 +99,7 @@ module IconsHelper
name << " fw" if fw
icon(name)
icon(name, options)
end
def file_type_icon_class(type, mode, name)
......
......@@ -107,23 +107,23 @@ module MarkupHelper
def markup(file_name, text, context = {})
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)
prepare_for_rendering(html, context)
end
def render_wiki_content(wiki_page)
def render_wiki_content(wiki_page, context = {})
text = wiki_page.content
return '' unless text.present?
context = {
context.merge!(
pipeline: :wiki,
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
issuable_state_filter_enabled: true,
markdown_engine: :redcarpet
}
issuable_state_filter_enabled: true
)
context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled?
html =
case wiki_page.format
......@@ -178,6 +178,10 @@ module MarkupHelper
end
end
def commonmark_for_repositories_enabled?
Feature.enabled?(:commonmark_for_repositories, default_enabled: true)
end
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
......
......@@ -254,6 +254,10 @@ module ProjectsHelper
"xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}"
end
def legacy_render_context(params)
params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
end
private
def get_project_nav_tabs(project, current_user)
......@@ -353,6 +357,10 @@ module ProjectsHelper
end
end
def default_clone_label
_("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase }
end
def default_clone_protocol
if allowed_protocols_present?
enabled_protocol
......
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
......@@ -11,6 +12,10 @@ module UserCalloutsHelper
!user_dismissed?(GCP_SIGNUP_OFFER)
end
def show_cluster_security_warning?
!user_dismissed?(CLUSTER_SECURITY_WARNING)
end
private
def user_dismissed?(feature_name)
......
......@@ -138,7 +138,7 @@ module VisibilityLevelHelper
end
def project_visibility_icon_description(level)
"#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
"#{project_visibility_level_description(level)}"
end
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
include Emails::Profile
include Emails::Pipelines
include Emails::Members
include Emails::AutoDevops
helper MergeRequestsHelper
helper DiffHelper
......
......@@ -127,6 +127,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message
end
private
def project
......
......@@ -2,6 +2,8 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
prepend EE::Blob
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
......
......@@ -90,7 +90,9 @@ module Ci
end
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
def expire_in
......
......@@ -174,6 +174,12 @@ module Ci
PipelineNotificationWorker.perform_async(pipeline.id)
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
scope :internal, -> { where(source: internal_sources) }
......
......@@ -32,7 +32,8 @@ module Clusters
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
files: files
files: files,
rbac: cluster.platform_kubernetes_rbac?
)
end
......
......@@ -39,6 +39,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
......
......@@ -40,6 +40,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
......
......@@ -50,6 +50,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files
)
......@@ -73,7 +74,7 @@ module Clusters
private
def kube_client
cluster&.kubeclient
cluster&.kubeclient&.core_client
end
end
end
......
......@@ -33,6 +33,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
repository: repository
......
......@@ -44,6 +44,7 @@ module Clusters
delegate :on_creation?, to: :provider, 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_ingress, prefix: true, allow_nil: true
......
......@@ -5,6 +5,7 @@ module Clusters
class Kubernetes < ActiveRecord::Base
include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil
prepend EE::KubernetesService
......@@ -49,6 +50,12 @@ module Clusters
alias_method :active?, :enabled?
enum_with_nil authorization_type: {
unknown_authorization: nil,
rbac: 1,
abac: 2
}
def actual_namespace
if namespace.present?
namespace
......@@ -97,7 +104,7 @@ module Clusters
end
def kubeclient
@kubeclient ||= build_kubeclient!
@kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
private
......@@ -117,15 +124,16 @@ module Clusters
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
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
unless (username && password) || token
raise "Either username/password or token is required to access API"
end
::Kubeclient::Client.new(
join_api_url(api_path),
Gitlab::Kubernetes::KubeClient.new(
api_url,
api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
......@@ -135,7 +143,7 @@ module Clusters
# Returns a hash of all pods in the namespace
def read_pods
kubeclient = build_kubeclient!
kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
......@@ -159,15 +167,6 @@ module Clusters
{ bearer_token: token }
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
{
token: token,
......
......@@ -22,6 +22,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
attr_accessor :redacted_full_title_html
attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
......
......@@ -9,23 +9,46 @@ module CaseSensitivity
#
# Unlike other ActiveRecord methods this method only operates on a Hash.
def iwhere(params)
criteria = self
cast_lower = Gitlab::Database.postgresql?
criteria = self
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 =
if cast_lower
"LOWER(#{column}) = LOWER(:value)"
else
"#{column} = :value"
end
private
def value_equal(column, value)
lower_value = lower_value(value)
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
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
......@@ -98,10 +98,10 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kubeclient!
kubeclient = build_kube_client!
kubeclient.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" }
kubeclient.core_client.discover
{ success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
end
......@@ -146,7 +146,7 @@ class KubernetesService < DeploymentService
end
def kubeclient
@kubeclient ||= build_kubeclient!
@kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
end
def deprecated?
......@@ -184,11 +184,12 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
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
::Kubeclient::Client.new(
join_api_url(api_path),
Gitlab::Kubernetes::KubeClient.new(
api_url,
api_groups,
api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
......@@ -198,7 +199,7 @@ class KubernetesService < DeploymentService
# Returns a hash of all pods in the namespace
def read_pods
kubeclient = build_kubeclient!
kubeclient = build_kube_client!
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue Kubeclient::HttpError => err
......@@ -222,15 +223,6 @@ class KubernetesService < DeploymentService
{ bearer_token: token }
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
{
token: token,
......
......@@ -585,7 +585,12 @@ class Repository
end
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
cache_method :rendered_readme
......
......@@ -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_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
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.
#
......@@ -457,11 +458,11 @@ class User < ActiveRecord::Base
end
def find_by_username(username)
iwhere(username: username).take
by_username(username).take
end
def find_by_username!(username)
iwhere(username: username).take!
by_username(username).take!
end
def find_by_personal_access_token(token_string)
......
......@@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
gcp_signup_offer: 2
gcp_signup_offer: 2,
cluster_security_warning: 3
}
validates :user, presence: true
......
This diff is collapsed.
......@@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity
erase_project_job_path(project, build)
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 :iid do |build|
build.merge_request.iid
......@@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity
def project
build.project
end
def can_create_build_terminal?
can?(current_user, :create_build_terminal, build) && build.has_terminal?
end
end
......@@ -411,6 +411,12 @@ class NotificationService
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)
recipients_for_pages_domain(domain).each do |user|
mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later
......
......@@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService
end
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
# 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 @@
= form_errors(@application_setting)
%fieldset
.form-group
.form-group.mb-2
.form-check
= f.check_box :version_check_enabled, class: 'form-check-input'
= f.label :version_check_enabled, class: 'form-check-label' do
......@@ -16,23 +16,26 @@
.form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do
Enable usage ping
= _('Enable usage ping')
.form-text.text-muted
- if can_be_configured
To help improve GitLab and its user experience, GitLab will
periodically collect usage information.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
about what information is shared with GitLab Inc. Visit
= link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
%p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
- usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
%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 }
%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
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
= succeed '.' do
= link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
.form-group
= _('The usage ping is disabled, and cannot be configured through this form.')
- deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
- deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= 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.mt-3
= 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.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-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."
.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."
- if devise_mapping.rememberable?
.remember-me
......
.omniauth-container
%p
%span.light
Sign in with &nbsp;
- providers = enabled_button_based_providers
.omniauth-container.prepend-top-15
%label.label-bold.d-block
Sign in with
- providers = enabled_button_based_providers
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
= 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}"
%fieldset.prepend-top-10.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
- 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
- if has_icon
= provider_image_tag(provider)
%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 @@
.devise-errors
= devise_error_messages!
.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."
.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.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
.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."
.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."
.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."
%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?
.form-group
= check_box_tag :terms_opt_in, '1', false, required: true
......@@ -35,8 +35,3 @@
= recaptcha_tags
.submit-container
= 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
%div{ class: container_class }
- if @cohorts
= render 'cohorts_table'
= render 'usage_ping'
- else
.bs-callout.bs-callout-warning.clearfix
%p
User cohorts are only shown when the
= link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank'
is enabled. To enable it and see user cohorts,
visit
= succeed '.' do
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
- usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= 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 }
- if current_user.admin?
- application_settings_path = 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
.col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index')
%h4 Usage ping is not enabled
%p
ConvDev is only shown when the
= link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank'
is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective
= link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
%h4= _('Usage ping is not enabled')
- if !current_user.admin?
%p
- usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
= 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
- page_title 'ConvDev Index'
- page_title _('ConvDev Index')
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container
- if show_callout?('convdev_intro_callout_dismissed')
- if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed')
= render 'callout'
.prepend-top-default
- if !Gitlab::CurrentSettings.usage_ping_enabled
- if !usage_ping_enabled
= render 'disabled'
- elsif @metric.blank?
= render 'no_data'
......
......@@ -18,16 +18,17 @@
%strong.fly-out-top-item-name
= _('ConvDev Index')
= nav_link(controller: :cohorts) do
= link_to instance_statistics_cohorts_path do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Cohorts')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
= link_to instance_statistics_cohorts_path do
%strong.fly-out-top-item-name
= _('Cohorts')
- if Gitlab::CurrentSettings.usage_ping_enabled
= nav_link(controller: :cohorts) do
= link_to instance_statistics_cohorts_path do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Cohorts')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
= link_to instance_statistics_cohorts_path do
%strong.fly-out-top-item-name
= _('Cohorts')
= 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 @@
- 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;" }
API
- failed = @pipeline.statuses.latest.failed
%tr
%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" }
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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