Commit 6214d523 authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into ee-38175-add-domain-field-to-auto-devops-application-setting

parents 18d0ee07 3ea5015f
......@@ -763,8 +763,9 @@ cache gems:
gitlab_git_test:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
variables:
SETUP_DB: "false"
before_script: []
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
......@@ -15,11 +15,13 @@ export default class SecretValues {
init() {
this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
if (this.revealButton) {
const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus);
this.updateDom(isRevealed);
this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
}
}
onRevealButtonClicked() {
const previousIsRevealed = convertPermissionToBoolean(
......
import $ from 'jquery';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import CreateItemDropdown from '../create_item_dropdown';
import SecretValues from '../behaviors/secret_values';
const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
function createEnvironmentItem(value) {
return {
title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
id: value,
text: value,
};
}
export default class VariableList {
constructor({
container,
formField,
}) {
this.$container = $(container);
this.formField = formField;
this.environmentDropdownMap = new WeakMap();
this.inputMap = {
id: {
selector: '.js-ci-variable-input-id',
default: '',
},
key: {
selector: '.js-ci-variable-input-key',
default: '',
},
value: {
selector: '.js-ci-variable-input-value',
default: '',
},
protected: {
selector: '.js-ci-variable-input-protected',
default: 'true',
},
environment: {
// We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class
// See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458
selector: `input[name="${this.formField}[variables_attributes][][environment]"]`,
default: '*',
},
_destroy: {
selector: '.js-ci-variable-input-destroy',
default: '',
},
};
this.secretValues = new SecretValues({
container: this.$container[0],
valueSelector: '.js-row:not(:last-child) .js-secret-value',
placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
});
}
init() {
this.bindEvents();
this.secretValues.init();
}
bindEvents() {
this.$container.find('.js-row').each((index, rowEl) => {
this.initRow(rowEl);
});
this.$container.on('click', '.js-row-remove-button', (e) => {
e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row'));
});
const inputSelector = Object.keys(this.inputMap)
.map(name => this.inputMap[name].selector)
.join(',');
// Remove any empty rows except the last row
this.$container.on('blur', inputSelector, (e) => {
const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
this.removeRow($row);
}
});
// Always make sure there is an empty last row
this.$container.on('input trigger-change', inputSelector, () => {
const $lastRow = this.$container.find('.js-row').last();
if (this.checkIfRowTouched($lastRow)) {
this.insertRow($lastRow);
}
});
}
initRow(rowEl) {
const $row = $(rowEl);
setupToggleButtons($row[0]);
const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) {
const createItemDropdown = new CreateItemDropdown({
$dropdown: $environmentSelect,
defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
fieldName: `${this.formField}[variables_attributes][][environment]`,
getData: (term, callback) => callback(this.getEnvironmentValues()),
createNewItemFromValue: createEnvironmentItem,
onSelect: () => {
// Refresh the other dropdowns in the variable list
// so they have the new value we just picked
this.refreshDropdownData();
$row.find(this.inputMap.environment.selector).trigger('trigger-change');
},
});
// Clear out any data that might have been left-over from the row clone
createItemDropdown.clearDropdown();
this.environmentDropdownMap.set($row[0], createItemDropdown);
}
}
insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults
Object.keys(this.inputMap).forEach((name) => {
const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default);
});
this.initRow($rowClone);
$row.after($rowClone);
}
removeRow($row) {
const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
// eslint-disable-next-line no-underscore-dangle
.find(this.inputMap._destroy.selector)
.val(true);
} else {
$row.remove();
}
}
checkIfRowTouched($row) {
return Object.keys(this.inputMap).some((name) => {
const entry = this.inputMap[name];
const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default;
});
}
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
return validRows.map((rowEl) => {
const resultant = {};
Object.keys(this.inputMap).forEach((name) => {
const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector);
if ($input.length) {
resultant[name] = $input.val();
}
});
return resultant;
});
}
getEnvironmentValues() {
const valueMap = this.$container.find(this.inputMap.environment.selector).toArray()
.reduce((prevValueMap, envInput) => ({
...prevValueMap,
[envInput.value]: envInput.value,
}), {});
return Object.keys(valueMap).map(createEnvironmentItem);
}
refreshDropdownData() {
this.$container.find('.js-row').each((index, rowEl) => {
const environmentDropdown = this.environmentDropdownMap.get(rowEl);
if (environmentDropdown) {
environmentDropdown.refreshData();
}
});
}
}
import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page
export default function setupNativeFormVariableList({
container,
formField = 'variables',
}) {
const $container = $(container);
const variableList = new VariableList({
container: $container,
formField,
});
variableList.init();
// Clear out the names in the empty last row so it
// doesn't get submitted and throw validation errors
$container.closest('form').on('submit trigger-submit', () => {
const $lastRow = $container.find('.js-row').last();
const isTouched = variableList.checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
}
});
}
......@@ -8,6 +8,8 @@ import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
import 'core-js/es6/map';
import 'core-js/es6/weak-map';
// Browser polyfills
import 'classlist-polyfill';
......
......@@ -22,9 +22,9 @@ import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first
import initApprovals from 'ee/approvals'; // eslint-disable-line import/first
import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line import/first
(function() {
var Dispatcher;
var Dispatcher;
(function() {
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
......@@ -72,46 +72,16 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
}
switch (page) {
case 'sessions:new':
import('./pages/sessions/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:boards:show':
case 'projects:boards:index':
import('./pages/projects/boards/index')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:environments:metrics':
import('./pages/projects/environments/metrics')
.then(callDefault)
.catch(fail);
break;
case 'projects:merge_requests:index':
import('./pages/projects/merge_requests/index')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:issues:index':
import('./pages/projects/issues/index')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:issues:show':
import('./pages/projects/issues/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'dashboard:milestones:index':
import('./pages/dashboard/milestones/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:index':
import('./pages/projects/milestones/index')
.then(callDefault)
......@@ -352,9 +322,6 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
shortcut_handler = true;
break;
case 'projects:show':
import('./pages/projects/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
// ee-start
initGeoInfoModal();
......@@ -389,9 +356,6 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
.catch(fail);
break;
case 'groups:show':
import('./pages/groups/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'groups:group_members:index':
......@@ -400,7 +364,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
.catch(fail);
break;
case 'projects:project_members:index':
import('./pages/projects/project_members/')
import('./pages/projects/project_members')
.then(callDefault)
.catch(fail);
break;
......@@ -681,7 +645,7 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
}
break;
case 'profiles':
import('./pages/profiles/index/')
import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
......@@ -736,8 +700,8 @@ import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line
return Dispatcher;
})();
})();
$(window).on('load', function() {
new Dispatcher();
});
}).call(window);
export default function initDispatcher() {
return new Dispatcher();
}
......@@ -25,7 +25,7 @@ export default {
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title">
<div class="branch-header-title str-truncated ref-name">
<icon
name="branch"
:size="12"
......
......@@ -30,7 +30,7 @@
<template>
<div class="ide-status-bar">
<div>
<div class="ref-name">
<icon
name="branch"
:size="12"
......
......@@ -147,7 +147,7 @@ you started editing. Would you like to create a new branch?`)"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
class="form-control multi-file-commit-message ref-name"
name="commit-message"
v-model="commitMessage"
placeholder="Commit message"
......
......@@ -56,6 +56,8 @@
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
} else if (this.file.type === 'tree') {
return 'folder';
}
return '';
},
......@@ -95,7 +97,7 @@
:colspan="submoduleColSpan"
>
<a
class="repo-file-name"
class="repo-file-name str-truncated"
>
<file-icon
:file-name="file.name"
......
......@@ -5,6 +5,8 @@ import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions from './editor_options';
import gitlabTheme from './themes/gl_theme';
export default class Editor {
static create(monaco) {
this.editorInstance = new Editor(monaco);
......@@ -24,6 +26,8 @@ export default class Editor {
this.decorationsController = new DecorationsController(this),
);
this.setupMonacoTheme();
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
}, 200);
......@@ -71,6 +75,12 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
......
export default {
themeName: 'gitlab',
monacoTheme: {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editorLineNumber.foreground': '#CCCCCC',
},
},
};
......@@ -36,7 +36,7 @@ import initBreadcrumbs from './breadcrumb';
// EE-only scripts
import 'ee/main';
import './dispatcher';
import initDispatcher from './dispatcher';
// eslint-disable-next-line global-require, import/no-commonjs
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
......@@ -268,4 +268,6 @@ $(() => {
removeFlashClickListener(flashEl);
});
}
initDispatcher();
});
/* eslint-disable no-new */
import Flash from './flash';
import flash from './flash';
import axios from './lib/utils/axios_utils';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
......@@ -78,26 +79,21 @@ export default class MiniPipelineGraph {
const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint;
return $.ajax({
dataType: 'json',
type: 'GET',
url: endpoint,
beforeSend: () => {
this.renderBuildsList(button, '');
this.toggleLoading(button);
},
success: (data) => {
axios.get(endpoint)
.then(({ data }) => {
this.toggleLoading(button);
this.renderBuildsList(button, data.html);
this.stopDropdownClickPropagation();
},
error: () => {
})
.catch(() => {
this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
new Flash('An error occurred while fetching the builds.', 'alert');
},
flash('An error occurred while fetching the builds.', 'alert');
});
}
......
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
import Raphael from './raphael';
export default (function() {
......@@ -26,16 +29,13 @@ export default (function() {
}
BranchGraph.prototype.load = function() {
return $.ajax({
url: this.options.url,
method: "get",
dataType: "json",
success: $.proxy(function(data) {
axios.get(this.options.url)
.then(({ data }) => {
$(".loading", this.element).hide();
this.prepareData(data.days, data.commits);
return this.buildGraph();
}, this)
});
this.buildGraph();
})
.catch(() => __('Error fetching network graph.'));
};
BranchGraph.prototype.prepareData = function(days, commits) {
......
......@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
......@@ -252,26 +253,20 @@ export default class Notes {
return;
}
this.refreshing = true;
return $.ajax({
url: this.notes_url,
headers: { 'X-Last-Fetched-At': this.last_fetched_at },
dataType: 'json',
success: (function(_this) {
return function(data) {
var notes;
notes = data.notes;
_this.last_fetched_at = data.last_fetched_at;
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) {
_this.renderNote(note);
axios.get(this.notes_url, {
headers: {
'X-Last-Fetched-At': this.last_fetched_at,
},
}).then(({ data }) => {
const notes = data.notes;
this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note));
this.refreshing = false;
}).catch(() => {
this.refreshing = false;
});
};
})(this)
}).always((function(_this) {
return function() {
return _this.refreshing = false;
};
})(this));
}
/**
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class NotificationsForm {
constructor() {
this.toggleCheckbox = this.toggleCheckbox.bind(this);
......@@ -27,15 +31,10 @@ export default class NotificationsForm {
saveEvent($checkbox, $parent) {
const form = $parent.parents('form:first');
return $.ajax({
url: form.attr('action'),
method: form.attr('method'),
dataType: 'json',
data: form.serialize(),
beforeSend: () => {
this.showCheckboxLoadingSpinner($parent);
},
}).done((data) => {
axios[form.attr('method')](form.attr('action'), form.serialize())
.then(({ data }) => {
$checkbox.enable();
if (data.saved) {
$parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
......@@ -45,6 +44,7 @@ export default class NotificationsForm {
.toggleClass('fa-spin fa-spinner fa-check is-done');
}, 2000);
}
});
})
.catch(() => flash(__('There was an error saving your notification settings.')));
}
}
import { getParameterByName } from '~/lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
import { removeParams } from './lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400;
......@@ -22,13 +23,12 @@ export default {
getOld() {
this.loading.show();
$.ajax({
type: 'GET',
url: this.url,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
axios.get(this.url, {
params: {
limit: this.limit,
offset: this.offset,
},
}).then(({ data }) => {
this.append(data.count, this.prepareData(data.html));
this.callback();
......@@ -38,8 +38,7 @@ export default {
} else {
this.loading.hide();
}
},
});
}).catch(() => this.loading.hide());
},
append(count, html) {
......
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
import flash from '../../../flash';
export default function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint');
const el = document.querySelector('.usage-data');
$.ajax({
type: 'GET',
url: usageDataUrl,
dataType: 'html',
success(html) {
$('.usage-data').html(html);
},
});
axios.get(el.dataset.endpoint, {
responseType: 'text',
}).then(({ data }) => {
el.innerHTML = data;
}).catch(() => flash(__('Error fetching usage ping data.')));
}
import projectSelect from '~/project_select';
export default projectSelect;
document.addEventListener('DOMContentLoaded', projectSelect);
......@@ -2,6 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
import { __ } from '../../../../locale';
import flash from '../../../../flash';
import axios from '../../../../lib/utils/axios_utils';
export default class Todos {
constructor() {
......@@ -59,18 +62,12 @@ export default class Todos {
const target = e.target;
target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
url: target.dataset.href,
dataType: 'json',
data: {
'_method': target.dataset.method,
},
success: (data) => {
axios[target.dataset.method](target.dataset.href)
.then(({ data }) => {
this.updateRowState(target);
return this.updateBadges(data);
},
});
this.updateBadges(data);
}).catch(() => flash(__('Error updating todo status.')));
}
updateRowState(target) {
......@@ -98,19 +95,15 @@ export default class Todos {
e.preventDefault();
const target = e.currentTarget;
const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
url: target.dataset.href,
dataType: 'json',
data: requestData,
success: (data) => {
axios[target.dataset.method](target.dataset.href, {
ids: this.todo_ids,
}).then(({ data }) => {
this.updateAllState(target, data);
return this.updateBadges(data);
},
});
this.updateBadges(data);
}).catch(() => flash(__('Error updating status for all todos.')));
}
updateAllState(target, data) {
......
......@@ -7,7 +7,7 @@ import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initGroupsList from '../../../groups';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
new ShortcutsNavigation();
new NotificationsForm();
......@@ -19,4 +19,4 @@ export default () => {
}
initGroupsList();
};
});
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
};
});
......@@ -7,10 +7,10 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.ISSUES);
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
new ShortcutsNavigation();
new UsersSelect();
};
});
/* eslint-disable no-new */
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Issue();
new ShortcutsIssuable();
new ZenMode();
initIssuableSidebar();
};
});
......@@ -5,9 +5,9 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS);
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
};
});
......@@ -8,7 +8,7 @@ import { ajaxGet } from '~/lib/utils/common_utils';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
......@@ -24,4 +24,4 @@ export default () => {
$('#tree-slider').waitForImages(() => {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
};
});
import initPathLocks from 'ee/path_locks';
import Vue from 'vue';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer';
......@@ -13,6 +15,26 @@ export default () => {
$('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath));
const commitPipelineStatusEl = document.getElementById('commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink != null) {
statusLink.remove();
// eslint-disable-next-line no-new
new Vue({
el: commitPipelineStatusEl,
components: {
commitPipelineStatus,
},
render(createElement) {
return createElement('commit-pipeline-status', {
props: {
endpoint: commitPipelineStatusEl.dataset.endpoint,
},
});
},
});
}
if (document.querySelector('.js-tree-content').dataset.pathLocksAvailable === 'true') {
initPathLocks(
document.querySelector('.js-tree-content').dataset.pathLocksToggle,
......
......@@ -2,10 +2,10 @@ import UsernameValidator from './username_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new OAuthRememberMe({ // eslint-disable-line no-new
container: $('.omniauth-container'),
}).bindEvents();
};
});
......@@ -4,7 +4,7 @@ import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
import { setupPipelineVariableList } from './setup_pipeline_variable_list';
import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
......@@ -42,5 +42,8 @@ document.addEventListener('DOMContentLoaded', () => {
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
setupNativeFormVariableList({
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
});
});
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
function insertRow($row) {
const $rowClone = $row.clone();
$rowClone.removeAttr('data-is-persisted');
$rowClone.find('input, textarea').val('');
$row.after($rowClone);
}
function removeRow($row) {
const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted'));
if (isPersisted) {
$row.hide();
$row
.find('.js-destroy-input')
.val(1);
} else {
$row.remove();
}
}
function checkIfRowTouched($row) {
return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0);
}
function setupPipelineVariableList(parent = document) {
const $parent = $(parent);
$parent.on('click', '.js-row-remove-button', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
removeRow($row);
e.preventDefault();
});
// Remove any empty rows except the last r
$parent.on('blur', '.js-user-input', (e) => {
const $row = $(e.currentTarget).closest('.js-row');
const isTouched = checkIfRowTouched($row);
if ($row.is(':not(:last-child)') && !isTouched) {
removeRow($row);
}
});
// Always make sure there is an empty last row
$parent.on('input', '.js-user-input', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (isTouched) {
insertRow($lastRow);
}
});
// Clear out the empty last row so it
// doesn't get submitted and throw validation errors
$parent.closest('form').on('submit', () => {
const $lastRow = $parent.find('.js-row').last();
const isTouched = checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
}
});
}
export {
setupPipelineVariableList,
insertRow,
removeRow,
};
<script>
import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
directives: {
tooltip,
},
components: {
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
/* This prop can be used to replace some of the `render_commit_status`
used across GitLab, this way we could use this vue component and add a
realtime status where it makes sense
realtime: {
type: Boolean,
required: false,
default: true,
}, */
},
data() {
return {
ciStatus: {},
isLoading: true,
};
},
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
},
},
mounted() {
this.service = new CommitPipelineService(this.endpoint);
this.initPolling();
},
methods: {
successCallback(res) {
const pipelines = res.data.pipelines;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
}
this.isLoading = false;
},
errorCallback() {
this.ciStatus = {
text: 'not found',
icon: 'status_notfound',
group: 'notfound',
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
},
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchPipelineCommitData();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
fetchPipelineCommitData() {
this.service.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
},
},
destroy() {
this.poll.stop();
},
};
</script>
<template>
<div>
<loading-icon
label="Loading pipeline status"
size="3"
v-if="isLoading"
/>
<a
v-else
:href="ciStatus.details_path"
>
<ci-icon
v-tooltip
:title="statusTitle"
:aria-label="statusTitle"
data-container="body"
:status="ciStatus"
/>
</a>
</div>
</template>
import axios from '~/lib/utils/axios_utils';
export default class CommitPipelineService {
constructor(endpoint) {
this.endpoint = endpoint;
}
fetchData() {
return axios.get(this.endpoint);
}
}
......@@ -13,7 +13,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
```
*/
function updatetoggle(toggle, isOn) {
function updateToggle(toggle, isOn) {
toggle.classList.toggle('is-checked', isOn);
}
......@@ -21,7 +21,7 @@ function onToggleClicked(toggle, input, clickCallback) {
const previousIsOn = convertPermissionToBoolean(input.value);
// Visually change the toggle and start loading
updatetoggle(toggle, !previousIsOn);
updateToggle(toggle, !previousIsOn);
toggle.setAttribute('disabled', true);
toggle.classList.toggle('is-loading', true);
......@@ -32,7 +32,7 @@ function onToggleClicked(toggle, input, clickCallback) {
})
.catch(() => {
// Revert the visuals if something goes wrong
updatetoggle(toggle, previousIsOn);
updateToggle(toggle, previousIsOn);
})
.then(() => {
// Remove the loading indicator in any case
......@@ -54,7 +54,7 @@ export default function setupToggleButtons(container, clickCallback = () => {})
const isOn = convertPermissionToBoolean(input.value);
// Get the visible toggle in sync with the hidden input
updatetoggle(toggle, isOn);
updateToggle(toggle, isOn);
toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback));
});
......
import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'MRWidgetAuthor',
props: {
author: { type: Object, required: true },
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
directives: {
tooltip,
},
template: `
<a
:href="author.webUrl || author.web_url"
class="author-link inline"
:v-tooltip="showAuthorTooltip"
:title="author.name">
<img
:src="author.avatarUrl || author.avatar_url"
class="avatar avatar-inline s16" />
<span
v-if="showAuthorName"
class="author">{{author.name}}
</span>
</a>
`,
};
<script>
import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'MRWidgetAuthor',
directives: {
tooltip,
},
props: {
author: {
type: Object,
required: true,
},
showAuthorName: {
type: Boolean,
required: false,
default: true,
},
showAuthorTooltip: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
authorUrl() {
return this.author.webUrl || this.author.web_url;
},
avatarUrl() {
return this.author.avatarUrl || this.author.avatar_url;
},
},
};
</script>
<template>
<a
:href="authorUrl"
class="author-link inline"
:v-tooltip="showAuthorTooltip"
:title="author.name"
>
<img
:src="avatarUrl"
class="avatar avatar-inline s16"
/>
<span
class="author"
v-if="showAuthorName"
>
{{ author.name }}
</span>
</a>
</template>
import MRWidgetAuthor from './mr_widget_author';
export default {
name: 'MRWidgetAuthorTime',
props: {
actionText: { type: String, required: true },
author: { type: Object, required: true },
dateTitle: { type: String, required: true },
dateReadable: { type: String, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
template: `
<h4 class="js-mr-widget-author">
{{actionText}}
<mr-widget-author :author="author" />
<time
:title="dateTitle"
data-toggle="tooltip"
data-placement="top"
data-container="body">
{{dateReadable}}
</time>
</h4>
`,
};
<script>
import mrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MRWidgetAuthorTime',
components: {
mrWidgetAuthor,
},
props: {
actionText: {
type: String,
required: true,
},
author: {
type: Object,
required: true,
},
dateTitle: {
type: String,
required: true,
},
dateReadable: {
type: String,
required: true,
},
},
};
</script>
<template>
<h4 class="js-mr-widget-author">
{{ actionText }}
<mr-widget-author :author="author" />
<time
:title="dateTitle"
data-toggle="tooltip"
data-placement="top"
data-container="body"
>
{{ dateReadable }}
</time>
</h4>
</template>
import tooltip from '../../vue_shared/directives/tooltip';
import { pluralize } from '../../lib/utils/text_utility';
import icon from '../../vue_shared/components/icon.vue';
export default {
name: 'MRWidgetHeader',
props: {
mr: { type: Object, required: true },
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
template: `
<div class="mr-source-target">
<div class="normal">
<strong>
Request to merge
<span
class="label-branch"
:class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
v-html="mr.sourceBranchLink"></span>
<button
v-tooltip
class="btn btn-transparent btn-clipboard"
data-title="Copy branch name to clipboard"
:data-clipboard-text="branchNameClipboardData">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
into
<span
class="label-branch"
:v-tooltip="isBranchTitleLong(mr.sourceBranch)"
:class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
(<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
</span>
</div>
<div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<icon
name="download">
</icon>
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
:href="mr.emailPatchesPath"
download>
Email patches
</a>
</li>
<li>
<a
:href="mr.plainDiffPath"
download>
Plain diff
</a>
</li>
</ul>
</span>
</div>
</div>
`,
};
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script>
<template>
<div class="mr-source-target">
<div class="normal">
<strong>
{{ s__("mrWidget|Request to merge") }}
<span
class="label-branch js-source-branch"
:class="{ 'label-truncated': isSourceBranchLong }"
:title="isSourceBranchLong ? mr.sourceBranch : ''"
data-placement="bottom"
:v-tooltip="isSourceBranchLong"
v-html="mr.sourceBranchLink"
>
</span>
<clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
/>
{{ s__("mrWidget|into") }}
<span
class="label-branch"
:v-tooltip="isTargetBranchLong"
:class="{ 'label-truncatedtooltip': isTargetBranchLong }"
:title="isTargetBranchLong ? mr.targetBranch : ''"
data-placement="bottom"
>
<a
:href="mr.targetBranchTreePath"
class="js-target-branch"
>
{{ mr.targetBranch }}
</a>
</span>
</strong>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count"
>
(<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
</span>
</div>
<div v-if="mr.isOpen">
<button
data-target="#modal_merge_info"
data-toggle="modal"
:disabled="mr.sourceBranchRemoved"
class="btn btn-sm btn-default inline js-check-out-branch"
type="button"
>
{{ s__("mrWidget|Check out branch") }}
</button>
<span class="dropdown prepend-left-10">
<button
type="button"
class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
aria-haspopup="true"
aria-expanded="false"
>
<icon name="download" />
<i
class="fa fa-caret-down"
aria-hidden="true">
</i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
class="js-download-email-patches"
:href="mr.emailPatchesPath"
download
>
{{ s__("mrWidget|Email patches") }}
</a>
</li>
<li>
<a
class="js-download-plain-diff"
:href="mr.plainDiffPath"
download
>
{{ s__("mrWidget|Plain diff") }}
</a>
</li>
</ul>
</span>
</div>
</div>
</template>
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: { type: String, required: false, default: '' },
},
template: `
<section class="mr-widget-help">
<template
v-if="missingBranch">
If the {{missingBranch}} branch exists in your local repository, you
</template>
<template v-else>
You
</template>
can merge this merge request manually using the
<a
data-toggle="modal"
href="#modal_merge_info">
command line
</a>
</section>
`,
};
<script>
import { sprintf, s__ } from '~/locale';
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: {
type: String,
required: false,
default: '',
},
},
computed: {
missingBranchInfo() {
return sprintf(
s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'),
{ branch: this.missingBranch },
);
},
},
};
</script>
<template>
<section class="mr-widget-help">
<template v-if="missingBranch">
{{ missingBranchInfo }}
</template>
<template v-else>
{{ s__("mrWidget|You can merge this merge request manually using the") }}
</template>
<button
type="button"
class="btn-link btn-blank js-open-modal-help"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ s__("mrWidget|command line") }}
</button>
</section>
</template>
<script>
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
......
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetAuthor from '../../components/mr_widget_author';
import mrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
......
......@@ -3,7 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
......
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
......
......@@ -11,8 +11,8 @@
export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
......
<script>
import _ from 'underscore';
import { __, sprintf } from '~/locale';
export default {
props: {
inputId: {
type: String,
required: true,
},
confirmationKey: {
type: String,
required: true,
},
confirmationValue: {
type: String,
required: true,
},
shouldEscapeConfirmationValue: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
inputLabel() {
let value = this.confirmationValue;
if (this.shouldEscapeConfirmationValue) {
value = _.escape(value);
}
return sprintf(
__('Type %{value} to confirm:'),
{ value: `<code>${value}</code>` },
false,
);
},
},
methods: {
hasCorrectValue() {
return this.$refs.enteredValue.value === this.confirmationValue;
},
},
};
</script>
<template>
<div>
<label
v-html="inputLabel"
:for="inputId"
>
</label>
<input
:id="inputId"
:name="confirmationKey"
type="text"
ref="enteredValue"
class="form-control"
/>
</div>
</template>
......@@ -62,8 +62,7 @@
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
// We don't have a open folder icon yet
return this.opened ? 'folder' : 'folder';
return this.opened ? 'folder-open' : 'folder';
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
......
......@@ -60,3 +60,4 @@
@import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
@import "framework/sortable";
@import "framework/ci_variable_list";
......@@ -177,7 +177,8 @@
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}
&.btn-primary {
&.btn-primary,
&.btn-info {
@include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
}
}
......
.ci-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
clear: both;
}
.ci-variable-row {
display: flex;
align-items: flex-end;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-bottom: 3 * $gl-btn-padding;
}
}
&:last-child {
.ci-variable-body-item:last-child {
margin-right: $ci-variable-remove-button-width;
@media (max-width: $screen-xs-max) {
margin-right: 0;
}
}
.ci-variable-row-remove-button {
display: none;
}
@media (max-width: $screen-xs-max) {
.ci-variable-row-body {
margin-right: $ci-variable-remove-button-width;
}
}
}
}
.ci-variable-row-body {
display: flex;
width: 100%;
@media (max-width: $screen-xs-max) {
display: block;
}
}
.ci-variable-body-item {
flex: 1;
&:not(:last-child) {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-right: 0;
margin-bottom: $gl-btn-padding;
}
}
}
.ci-variable-protected-item {
flex: 0 1 auto;
display: flex;
align-items: center;
}
.ci-variable-row-remove-button {
@include transition(color);
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $ci-variable-remove-button-width;
height: $input-height;
padding: 0;
background: transparent;
border: 0;
color: $gl-text-color-secondary;
&:hover,
&:focus {
outline: none;
color: $gl-text-color;
}
}
......@@ -675,9 +675,9 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
/*
Pipeline Schedules
CI variable lists
*/
$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/*
......
......@@ -199,6 +199,18 @@
.commit-actions {
@media (min-width: $screen-sm-min) {
font-size: 0;
div {
display: inline;
}
.fa-spinner {
font-size: 12px;
}
span {
font-size: 6px;
}
}
.ci-status-link {
......@@ -223,6 +235,11 @@
font-size: 14px;
font-weight: $gl-font-weight-bold;
}
.ci-status-icon {
position: relative;
top: 1px;
}
}
.commit,
......
......@@ -410,7 +410,6 @@
width: 298px;
}
@media (max-width: $screen-xs-max) {
display: flex;
width: 100%;
......
......@@ -78,84 +78,3 @@
margin-right: 3px;
}
}
.pipeline-variable-list {
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
clear: both;
}
.pipeline-variable-row {
display: flex;
align-items: flex-end;
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
}
@media (max-width: $screen-sm-max) {
padding-right: $gl-col-padding;
}
&:last-child {
.pipeline-variable-row-remove-button {
display: none;
}
@media (max-width: $screen-sm-max) {
.pipeline-variable-value-input {
margin-right: $pipeline-variable-remove-button-width;
}
}
@media (max-width: $screen-xs-max) {
.pipeline-variable-row-body {
margin-right: $pipeline-variable-remove-button-width;
}
}
}
}
.pipeline-variable-row-body {
display: flex;
width: calc(75% - #{$gl-col-padding});
padding-left: $gl-col-padding;
@media (max-width: $screen-sm-max) {
width: 100%;
}
@media (max-width: $screen-xs-max) {
display: block;
}
}
.pipeline-variable-key-input {
margin-right: $gl-btn-padding;
@media (max-width: $screen-xs-max) {
margin-bottom: $gl-btn-padding;
}
}
.pipeline-variable-row-remove-button {
@include transition(color);
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: $pipeline-variable-remove-button-width;
height: $input-height;
padding: 0;
background: transparent;
border: 0;
color: $gl-text-color-secondary;
&:hover,
&:focus {
outline: none;
color: $gl-text-color;
}
}
......@@ -72,6 +72,13 @@
display: none;
}
}
&.folder {
svg {
fill: $gl-text-color-secondary;
}
}
}
a {
......@@ -522,7 +529,10 @@ table.table tr td.multi-file-table-name {
}
}
.ide.nav-only {
.ide {
overflow: hidden;
.nav-only {
.flash-container {
margin-top: $header-height;
margin-bottom: 0;
......@@ -539,6 +549,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
......@@ -552,6 +563,8 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
}
}
}
}
......@@ -571,6 +584,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
min-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
......@@ -584,6 +598,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
}
......
class Admin::CohortsController < Admin::ApplicationController
def index
if current_application_settings.usage_ping_enabled
if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
CohortsService.new.execute
end
......
......@@ -2,7 +2,6 @@ require 'gon'
require 'fogbugz'
class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
......@@ -28,7 +27,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
helper_method :can?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
......@@ -108,7 +107,7 @@ class ApplicationController < ActionController::Base
end
def verify_namespace_plan_check_enabled
render_404 unless current_application_settings.should_check_namespace_plan?
render_404 unless Gitlab::CurrentSettings.should_check_namespace_plan?
end
def log_exception(exception)
......@@ -127,7 +126,7 @@ class ApplicationController < ActionController::Base
if Gitlab::Geo.secondary?
Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
else
current_application_settings.after_sign_out_path.presence || new_user_session_path
Gitlab::CurrentSettings.after_sign_out_path.presence || new_user_session_path
end
end
......@@ -276,15 +275,15 @@ class ApplicationController < ActionController::Base
end
def import_sources_enabled?
!current_application_settings.import_sources.empty?
!Gitlab::CurrentSettings.import_sources.empty?
end
def github_import_enabled?
current_application_settings.import_sources.include?('github')
Gitlab::CurrentSettings.import_sources.include?('github')
end
def gitea_import_enabled?
current_application_settings.import_sources.include?('gitea')
Gitlab::CurrentSettings.import_sources.include?('gitea')
end
def github_import_configured?
......@@ -292,7 +291,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_import_enabled?
request.host != 'gitlab.com' && current_application_settings.import_sources.include?('gitlab')
request.host != 'gitlab.com' && Gitlab::CurrentSettings.import_sources.include?('gitlab')
end
def gitlab_import_configured?
......@@ -300,7 +299,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_enabled?
current_application_settings.import_sources.include?('bitbucket')
Gitlab::CurrentSettings.import_sources.include?('bitbucket')
end
def bitbucket_import_configured?
......@@ -308,19 +307,19 @@ class ApplicationController < ActionController::Base
end
def google_code_import_enabled?
current_application_settings.import_sources.include?('google_code')
Gitlab::CurrentSettings.import_sources.include?('google_code')
end
def fogbugz_import_enabled?
current_application_settings.import_sources.include?('fogbugz')
Gitlab::CurrentSettings.import_sources.include?('fogbugz')
end
def git_import_enabled?
current_application_settings.import_sources.include?('git')
Gitlab::CurrentSettings.import_sources.include?('git')
end
def gitlab_project_import_enabled?
current_application_settings.import_sources.include?('gitlab_project')
Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
# U2F (universal 2nd factor) devices need a unique identifier for the application
......
......@@ -20,13 +20,13 @@ module EnforcesTwoFactorAuthentication
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication? ||
Gitlab::CurrentSettings.require_two_factor_authentication? ||
current_user.try(:require_two_factor_authentication_from_group?)
end
def two_factor_authentication_reason(global: -> {}, group: -> {})
if two_factor_authentication_required?
if current_application_settings.require_two_factor_authentication?
if Gitlab::CurrentSettings.require_two_factor_authentication?
global.call
else
groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc)
......@@ -36,7 +36,7 @@ module EnforcesTwoFactorAuthentication
end
def two_factor_grace_period
periods = [current_application_settings.two_factor_grace_period]
periods = [Gitlab::CurrentSettings.two_factor_grace_period]
periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?)
periods.min
end
......
module RequiresWhitelistedMonitoringClient
extend ActiveSupport::Concern
include Gitlab::CurrentSettings
included do
before_action :validate_ip_whitelisted_or_valid_token!
end
......@@ -26,7 +24,7 @@ module RequiresWhitelistedMonitoringClient
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
Gitlab::CurrentSettings.health_check_access_token
)
end
......
module UploadsActions
include Gitlab::Utils::StrongMemoize
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
......@@ -17,34 +19,71 @@ module UploadsActions
end
end
# This should either
# - send the file directly
# - or redirect to its URL
#
def show
return render_404 unless uploader.exists?
if uploader.file_storage?
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
else
redirect_to uploader.url
end
end
private
def uploader_class
raise NotImplementedError
end
def upload_mount
mounted_as = params[:mounted_as]
mounted_as if UPLOAD_MOUNTS.include?(mounted_as)
end
def uploader_mounted?
upload_model_class < CarrierWave::Mount::Extension && !upload_mount.nil?
end
def uploader
strong_memoize(:uploader) do
return if show_model.nil?
if uploader_mounted?
model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
else
build_uploader_from_upload || build_uploader_from_params
end
end
end
file_uploader = FileUploader.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
def build_uploader_from_upload
return nil unless params[:secret] && params[:filename]
file_uploader
upload_path = uploader_class.upload_path(params[:secret], params[:filename])
upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path)
upload&.build_uploader
end
def build_uploader_from_params
uploader = uploader_class.new(model, params[:secret])
uploader.retrieve_from_store!(params[:filename])
uploader
end
def image_or_video?
uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
FileUploader
def find_model
nil
end
def model
strong_memoize(:model) { find_model }
end
end
......@@ -7,29 +7,23 @@ class Groups::UploadsController < Groups::ApplicationController
private
def show_model
strong_memoize(:show_model) do
def upload_model_class
Group
end
def uploader_class
NamespaceFileUploader
end
def find_model
return @group if @group
group_id = params[:group_id]
Group.find_by_full_path(group_id)
end
end
def authorize_upload_file!
render_404 unless can?(current_user, :upload_file, group)
end
def uploader
strong_memoize(:uploader) do
file_uploader = uploader_class.new(show_model, params[:secret])
file_uploader.retrieve_from_store!(params[:filename])
file_uploader
end
end
def uploader_class
NamespaceFileUploader
end
alias_method :model, :group
end
......@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user
notice = "To accept this invitation, sign in"
notice << " or create an account" if current_application_settings.allow_signup?
notice << " or create an account" if Gitlab::CurrentSettings.allow_signup?
notice << "."
store_location_for :user, request.fullpath
......
......@@ -10,6 +10,6 @@ class KodingController < ApplicationController
private
def check_integration!
render_404 unless current_application_settings.koding_enabled?
render_404 unless Gitlab::CurrentSettings.koding_enabled?
end
end
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
include OauthApplications
......@@ -31,7 +30,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
private
def verify_user_oauth_applications_enabled
return if current_application_settings.user_oauth_applications?
return if Gitlab::CurrentSettings.user_oauth_applications?
redirect_to profile_path
end
......
......@@ -158,7 +158,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.allow_signup?
if Gitlab::CurrentSettings.allow_signup?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
......
class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
skip_before_action :require_no_authentication, only: [:edit, :update]
before_action :resource_from_email, only: [:create]
......@@ -48,7 +46,7 @@ class PasswordsController < Devise::PasswordsController
if resource
return if resource.allow_password_authentication?
else
return if current_application_settings.password_authentication_enabled?
return if Gitlab::CurrentSettings.password_authentication_enabled?
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
......
......@@ -61,7 +61,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def store_file(oid, size, tmp_file)
# Define tmp_file_path early because we use it in "ensure"
tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file)
object = LfsObject.find_or_create_by(oid: oid, size: size)
file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
......
......@@ -24,7 +24,7 @@ module Projects
end
def slack_service
if current_application_settings.slack_app_enabled
if Gitlab::CurrentSettings.slack_app_enabled
'slack_slash_commands'
else
'gitlab_slack_application'
......
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
# These will kick you out if you don't have access.
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
......@@ -8,14 +9,20 @@ class Projects::UploadsController < Projects::ApplicationController
private
def show_model
strong_memoize(:show_model) do
def upload_model_class
Project
end
def uploader_class
FileUploader
end
def find_model
return @project if @project
namespace = params[:namespace_id]
id = params[:project_id]
Project.find_by_full_path("#{namespace}/#{id}")
end
end
alias_method :model, :project
end
......@@ -404,7 +404,7 @@ class ProjectsController < Projects::ApplicationController
end
def project_export_enabled
render_404 unless current_application_settings.project_export_enabled?
render_404 unless Gitlab::CurrentSettings.project_export_enabled?
end
def redirect_git_extension
......
......@@ -23,7 +23,7 @@ class RootController < Dashboard::ProjectsController
def redirect_unlogged_user
if redirect_to_home_page_url?
redirect_to(current_application_settings.home_page_url)
redirect_to(Gitlab::CurrentSettings.home_page_url)
else
redirect_to(new_user_session_path)
end
......@@ -48,9 +48,9 @@ class RootController < Dashboard::ProjectsController
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
return false unless current_application_settings.home_page_url.present?
return false unless Gitlab::CurrentSettings.home_page_url.present?
home_page_url = current_application_settings.home_page_url.chomp('/')
home_page_url = Gitlab::CurrentSettings.home_page_url.chomp('/')
root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
root_urls.exclude?(home_page_url)
......
class UploadsController < ApplicationController
include UploadsActions
UnknownUploadModelError = Class.new(StandardError)
MODEL_CLASSES = {
"user" => User,
"project" => Project,
"note" => Note,
"group" => Group,
"appearance" => Appearance,
"personal_snippet" => PersonalSnippet,
nil => PersonalSnippet
}.freeze
rescue_from UnknownUploadModelError, with: :render_404
skip_before_action :authenticate_user!
before_action :upload_mount_satisfied?
before_action :find_model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create]
private
def uploader_class
PersonalFileUploader
end
def find_model
return nil unless params[:id]
return render_404 unless upload_model && upload_mount
@model = upload_model.find(params[:id])
upload_model_class.find(params[:id])
end
def authorize_access!
......@@ -53,55 +68,17 @@ class UploadsController < ApplicationController
end
end
def upload_model
upload_models = {
"user" => User,
"project" => Project,
"note" => Note,
"group" => Group,
"appearance" => Appearance,
"personal_snippet" => PersonalSnippet
}
upload_models[params[:model]]
end
def upload_mount
return true unless params[:mounted_as]
upload_mounts = %w(avatar attachment file logo header_logo)
if upload_mounts.include?(params[:mounted_as])
params[:mounted_as]
end
end
def uploader
return @uploader if defined?(@uploader)
case model
when nil
@uploader = PersonalFileUploader.new(nil, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
when PersonalSnippet
@uploader = PersonalFileUploader.new(model, params[:secret])
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
redirect_to @uploader.url unless @uploader.file_storage?
def upload_model_class
MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
end
@uploader
def upload_model_class_has_mounts?
upload_model_class < CarrierWave::Mount::Extension
end
def uploader_class
PersonalFileUploader
end
def upload_mount_satisfied?
return true unless upload_model_class_has_mounts?
def model
@model ||= find_model
upload_model_class.uploader_options.has_key?(upload_mount)
end
end
......@@ -2,25 +2,23 @@ module ApplicationSettingsHelper
prepend EE::ApplicationSettingsHelper
extend self
include Gitlab::CurrentSettings
delegate :allow_signup?,
:gravatar_enabled?,
:password_authentication_enabled_for_web?,
:akismet_enabled?,
:koding_enabled?,
to: :current_application_settings
to: :'Gitlab::CurrentSettings.current_application_settings'
def user_oauth_applications?
current_application_settings.user_oauth_applications
Gitlab::CurrentSettings.user_oauth_applications
end
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
Gitlab::CurrentSettings.enabled_git_access_protocol.present?
end
def enabled_protocol
case current_application_settings.enabled_git_access_protocol
case Gitlab::CurrentSettings.enabled_git_access_protocol
when 'http'
gitlab_config.protocol
when 'ssh'
......@@ -58,7 +56,7 @@ module ApplicationSettingsHelper
# toggle button effect.
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
checked = Gitlab::CurrentSettings.import_sources.include?(source)
css_class = checked ? 'active' : ''
checkbox_name = 'application_setting[import_sources][]'
......@@ -73,7 +71,7 @@ module ApplicationSettingsHelper
def oauth_providers_checkboxes
button_based_providers.map do |source|
disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s)
disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s)
css_class = 'btn'
css_class << ' active' unless disabled
checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]'
......
module AuthHelper
include Gitlab::CurrentSettings
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
delegate :slack_app_id, to: :current_application_settings
delegate :slack_app_id, to: :'Gitlab::CurrentSettings.current_application_settings'
def ldap_enabled?
Gitlab::LDAP::Config.enabled?
......@@ -47,7 +45,7 @@ module AuthHelper
end
def enabled_button_based_providers
disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || []
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
button_based_providers.map(&:to_s) - disabled_providers
end
......
module ProjectsHelper
include Gitlab::CurrentSettings
prepend ::EE::ProjectsHelper
def link_to_project(project)
......@@ -216,7 +214,7 @@ module ProjectsHelper
project.cache_key,
controller.controller_name,
controller.action_name,
current_application_settings.cache_key,
Gitlab::CurrentSettings.cache_key,
'v2.5'
]
......@@ -468,10 +466,10 @@ module ProjectsHelper
path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
return URI.join(current_application_settings.koding_url, path).to_s
return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s
end
current_application_settings.koding_url
Gitlab::CurrentSettings.koding_url
end
def contribution_guide_path(project)
......@@ -588,7 +586,7 @@ module ProjectsHelper
def restricted_levels
return [] if current_user.admin?
current_application_settings.restricted_visibility_levels || []
Gitlab::CurrentSettings.restricted_visibility_levels || []
end
def project_permissions_settings(project)
......
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
end
......
......@@ -151,12 +151,12 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
current_application_settings.restricted_visibility_levels || []
Gitlab::CurrentSettings.restricted_visibility_levels || []
end
delegate :default_project_visibility,
:default_group_visibility,
to: :current_application_settings
to: :'Gitlab::CurrentSettings.current_application_settings'
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
......
......@@ -5,6 +5,24 @@ module WebpackHelper
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
end
def webpack_controller_bundle_tags
bundles = []
segments = [*controller.controller_path.split('/'), controller.action_name].compact
until segments.empty?
begin
asset_paths = gitlab_webpack_asset_paths("pages.#{segments.join('.')}", extension: 'js')
bundles.unshift(*asset_paths)
rescue Webpack::Rails::Manifest::EntryPointMissingError
# no bundle exists for this path
end
segments.pop
end
javascript_include_tag(*bundles)
end
# override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present?
......
class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings
def notify(abuse_report_id)
return unless deliverable?
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
to: current_application_settings.admin_notification_email,
to: Gitlab::CurrentSettings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
......@@ -15,6 +13,6 @@ class AbuseReportMailer < BaseMailer
private
def deliverable?
current_application_settings.admin_notification_email.present?
Gitlab::CurrentSettings.admin_notification_email.present?
end
end
class BaseMailer < ActionMailer::Base
include Gitlab::CurrentSettings
around_action :render_with_default_locale
helper ApplicationHelper
helper MarkupHelper
attr_accessor :current_user
helper_method :current_user, :can?, :current_application_settings
helper_method :current_user, :can?
default from: proc { default_sender_address.format }
default reply_to: proc { default_reply_to_address.format }
......
......@@ -11,6 +11,7 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = 'current_appearance'.freeze
......
......@@ -49,7 +49,7 @@ module Ci
end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, LegacyArtifactUploader::LOCAL_STORE]) }
scope :with_artifacts_stored_locally, ->() { with_artifacts.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
......
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
......@@ -171,7 +170,7 @@ module Clusters
{
token: token,
ca_pem: ca_pem,
max_session_time: current_application_settings.terminal_max_session_time
max_session_time: Gitlab::CurrentSettings.terminal_max_session_time
}
end
......
module Avatarable
extend ActiveSupport::Concern
included do
prepend ShadowMethods
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
end
module ShadowMethods
def avatar_url(**args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
avatar_path(only_path: args.fetch(:only_path, true)) || super
end
end
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, "only images allowed"
end
end
def avatar_path(only_path: true)
return unless self[:avatar].present?
......
......@@ -34,26 +34,21 @@ class Group < Namespace
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
......@@ -133,12 +128,6 @@ class Group < Namespace
visibility_level_allowed_by_sub_groups?(level)
end
def avatar_url(**args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
avatar_path(args)
end
def lfs_enabled?
return false unless Gitlab.config.lfs.enabled
return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
......@@ -211,12 +200,6 @@ class Group < Namespace
owners.include?(user) && owners.size == 1
end
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, "only images allowed"
end
end
def human_ldap_access
Gitlab::Access.options_with_owner.key ldap_access
end
......@@ -247,7 +230,7 @@ class Group < Namespace
end
def actual_size_limit
return current_application_settings.repository_size_limit if repository_size_limit.nil?
return Gitlab::CurrentSettings.repository_size_limit if repository_size_limit.nil?
repository_size_limit
end
......
require 'digest/md5'
class Key < ActiveRecord::Base
include Gitlab::CurrentSettings
include AfterCommitQueue
include Sortable
......@@ -107,7 +106,7 @@ class Key < ActiveRecord::Base
end
def key_meets_restrictions
restriction = current_application_settings.key_restriction_for(public_key.type)
restriction = Gitlab::CurrentSettings.key_restriction_for(public_key.type)
if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE
errors.add(:key, forbidden_key_type_message)
......@@ -118,7 +117,7 @@ class Key < ActiveRecord::Base
def forbidden_key_type_message
allowed_types =
current_application_settings
Gitlab::CurrentSettings
.allowed_key_types
.map(&:upcase)
.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
......
......@@ -7,7 +7,7 @@ class LfsObject < ActiveRecord::Base
validates :oid, presence: true, uniqueness: true
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::LOCAL_STORE]) }
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
mount_uploader :file, LfsObjectUploader
......
......@@ -3,7 +3,6 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
include Gitlab::CurrentSettings
include Gitlab::VisibilityLevel
include Routable
include AfterCommitQueue
......@@ -162,7 +161,7 @@ class Namespace < ActiveRecord::Base
end
def actual_size_limit
current_application_settings.repository_size_limit
Gitlab::CurrentSettings.repository_size_limit
end
def shared_runners_enabled?
......
......@@ -5,7 +5,6 @@ class Note < ActiveRecord::Base
extend ActiveModel::Naming
prepend EE::Note
include Gitlab::CurrentSettings
include Participable
include Mentionable
include Elastic::NotesSearch
......@@ -91,6 +90,7 @@ class Note < ActiveRecord::Base
end
end
# @deprecated attachments are handler by the MarkdownUploader
mount_uploader :attachment, AttachmentUploader
# Scopes
......@@ -203,7 +203,7 @@ class Note < ActiveRecord::Base
end
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
def hook_attrs
......
......@@ -4,7 +4,6 @@ class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
include Avatarable
include CacheMarkdownField
......@@ -26,7 +25,6 @@ class Project < ActiveRecord::Base
prepend EE::Project
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
BoardLimitExceeded = Class.new(StandardError)
......@@ -54,8 +52,8 @@ class Project < ActiveRecord::Base
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
......@@ -261,9 +259,6 @@ class Project < ActiveRecord::Base
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
validate :check_wiki_path_conflict
......@@ -271,7 +266,6 @@ class Project < ActiveRecord::Base
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Scopes
......@@ -499,14 +493,14 @@ class Project < ActiveRecord::Base
def auto_devops_enabled?
if auto_devops&.enabled.nil?
current_application_settings.auto_devops_enabled?
Gitlab::CurrentSettings.auto_devops_enabled?
else
auto_devops.enabled?
end
end
def has_auto_devops_implicitly_disabled?
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
end
def empty_repo?
......@@ -933,20 +927,12 @@ class Project < ActiveRecord::Base
issues_tracker.to_param == 'jira'
end
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, 'only images allowed'
end
end
def avatar_in_git
repository.avatar
end
def avatar_url(**args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
end
# For compatibility with old code
......@@ -1495,14 +1481,14 @@ class Project < ActiveRecord::Base
# Ensure HEAD points to the default branch in case it is not master
change_head(default_branch)
if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
params = {
name: default_branch,
push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}],
merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
}]
}
......@@ -1791,7 +1777,7 @@ class Project < ActiveRecord::Base
end
def use_hashed_storage
if self.new_record? && current_application_settings.hashed_storage_enabled
if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
self.storage_version = LATEST_STORAGE_VERSION
end
end
......
......@@ -4,7 +4,6 @@
# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < DeploymentService
include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
......@@ -233,7 +232,7 @@ class KubernetesService < DeploymentService
{
token: token,
ca_pem: ca_pem,
max_session_time: current_application_settings.terminal_max_session_time
max_session_time: Gitlab::CurrentSettings.terminal_max_session_time
}
end
......
......@@ -4,7 +4,6 @@ class ProjectWiki
# EE only modules
include Elastic::WikiRepositoriesSearch
include Gitlab::CurrentSettings
MARKUPS = {
'Markdown' => :markdown,
......@@ -204,7 +203,7 @@ class ProjectWiki
# EE only
def update_elastic_index
index_blobs if current_application_settings.elasticsearch_indexing?
index_blobs if Gitlab::CurrentSettings.elasticsearch_indexing?
end
def path_to_repo
......
......@@ -3,8 +3,6 @@ class ProtectedBranch < ActiveRecord::Base
include ProtectedRef
prepend EE::ProtectedRef
extend Gitlab::CurrentSettings
protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system
......@@ -17,7 +15,7 @@ class ProtectedBranch < ActiveRecord::Base
end
def self.default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
end
......@@ -12,8 +12,6 @@ class Snippet < ActiveRecord::Base
include Editable
include Gitlab::SQL::Pattern
extend Gitlab::CurrentSettings
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content
......@@ -29,7 +27,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
end
default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_snippet_visibility }
belongs_to :author, class_name: 'User'
belongs_to :project
......
......@@ -9,50 +9,54 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
before_save :calculate_checksum, if: :foreground_checksum?
after_commit :schedule_checksum, unless: :foreground_checksum?
scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
def self.remove_path(path)
where(path: path).destroy_all
end
def self.record(uploader)
remove_path(uploader.relative_path)
create(
size: uploader.file.size,
path: uploader.relative_path,
model: uploader.model,
uploader: uploader.class.to_s
)
end
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
def self.hexdigest(absolute_path)
return unless File.exist?(absolute_path)
Digest::SHA256.file(absolute_path).hexdigest
def self.hexdigest(path)
Digest::SHA256.file(path).hexdigest
end
def absolute_path
raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path?
uploader_class.absolute_path(self)
end
def calculate_checksum
return unless exist?
def calculate_checksum!
self.checksum = nil
return unless checksummable?
self.checksum = self.class.hexdigest(absolute_path)
end
def build_uploader
uploader_class.new(model).tap do |uploader|
uploader.upload = self
uploader.retrieve_from_store!(identifier)
end
end
def exist?
File.exist?(absolute_path)
end
private
def foreground_checksum?
size <= CHECKSUM_THRESHOLD
def checksummable?
checksum.nil? && local? && exist?
end
def local?
return true if store.nil?
store == ObjectStorage::Store::LOCAL
end
def foreground_checksummable?
checksummable? && size <= CHECKSUM_THRESHOLD
end
def schedule_checksum
......@@ -63,6 +67,10 @@ class Upload < ActiveRecord::Base
!path.start_with?('/')
end
def identifier
File.basename(path)
end
def uploader_class
Object.const_get(uploader)
end
......
......@@ -2,10 +2,8 @@ require 'carrierwave/orm/activerecord'
class User < ActiveRecord::Base
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
include Gitlab::ConfigHelper
include Gitlab::CurrentSettings
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
......@@ -32,7 +30,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :rss_token
default_value_for :admin, false
default_value_for(:external) { current_application_settings.user_default_external }
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
......@@ -139,6 +137,7 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
#
# Validations
......@@ -161,12 +160,10 @@ class User < ActiveRecord::Base
validate :namespace_uniq, if: :username_changed?
validate :namespace_move_dir_allowed, if: :username_changed?
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: :email_changed?
validate :owns_notification_email, if: :notification_email_changed?
validate :owns_public_email, if: :public_email_changed?
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
......@@ -229,9 +226,6 @@ class User < ActiveRecord::Base
end
end
mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Scopes
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
......@@ -545,12 +539,6 @@ class User < ActiveRecord::Base
end
end
def avatar_type
unless avatar.image?
errors.add :avatar, "only images allowed"
end
end
def unique_email
if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, 'has already been taken')
......@@ -688,11 +676,11 @@ class User < ActiveRecord::Base
end
def allow_password_authentication_for_web?
current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user?
end
def allow_password_authentication_for_git?
current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
......@@ -820,7 +808,7 @@ class User < ActiveRecord::Base
# without this safeguard!
return unless has_attribute?(:projects_limit) && projects_limit.nil?
self.projects_limit = current_application_settings.default_projects_limit
self.projects_limit = Gitlab::CurrentSettings.default_projects_limit
end
def requires_ldap_check?
......@@ -878,9 +866,7 @@ class User < ActiveRecord::Base
end
def avatar_url(size: nil, scale: 2, **args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
GravatarService.new.execute(email, size, scale, username: username)
end
def primary_email_verified?
......@@ -1249,7 +1235,7 @@ class User < ActiveRecord::Base
else
# Only revert these back to the default if they weren't specifically changed in this update.
self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
end
end
......@@ -1257,15 +1243,15 @@ class User < ActiveRecord::Base
valid = true
error = nil
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
if Gitlab::CurrentSettings.domain_blacklist_enabled?
blocked_domains = Gitlab::CurrentSettings.domain_blacklist
if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
end
allowed_domains = current_application_settings.domain_whitelist
allowed_domains = Gitlab::CurrentSettings.domain_whitelist
unless allowed_domains.blank?
if domain_matches?(allowed_domains, email)
valid = true
......
......@@ -10,6 +10,10 @@ module Ci
can?(:developer_access) && pipeline_schedule.owned_by?(@user)
end
condition(:non_owner_of_schedule) do
!pipeline_schedule.owned_by?(@user)
end
rule { can?(:developer_access) }.policy do
enable :play_pipeline_schedule
end
......@@ -19,6 +23,10 @@ module Ci
enable :admin_pipeline_schedule
end
rule { can?(:master_access) & non_owner_of_schedule }.policy do
enable :take_ownership_pipeline_schedule
end
rule { protected_ref }.prevent :play_pipeline_schedule
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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