Commit a3d80768 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 598e33d9 13647d24
......@@ -297,9 +297,9 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
......@@ -677,7 +677,7 @@ When your code contains more than 500 changes, any major breaking changes, or an
[license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing.md
[testing]: doc/development/testing_guide/index.md
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
## GitLab Core Team & GitLab Inc. Contribution Process
## GitLab core team & GitLab Inc. contribution process
---
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import Flash from './flash';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......
/* global Flash */
import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
function onError() {
const flash = new window.Flash('Balsamiq file could not be loaded.');
const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import Flash from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
......
/* global Flash */
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
export default class BlobViewer {
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
/* global Flash */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import './models/issue';
......
......@@ -3,9 +3,9 @@
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
......
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
const ModalStore = gl.issueBoards.ModalStore;
......
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../../../flash';
const Store = gl.issueBoards.BoardsStore;
......
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
......
/* global Flash */
import Vue from 'vue';
import Cookies from 'js-cookie';
import Flash from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
......
<script>
/* global Flash */
import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
......
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
......
/* global Flash */
/* global CommentsStore */
import Vue from 'vue';
import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
window.gl = window.gl || {};
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......
/* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownEmoji extends gl.FilteredSearchDropdown {
......
/* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
......
/* global Flash */
import AjaxFilter from '~/droplab/plugins/ajax_filter';
import Flash from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
......
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
......@@ -44,7 +45,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new window.Flash('An error occurred while parsing recent searches');
new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
......
import AjaxCache from '../lib/utils/ajax_cache';
import '../flash'; /* global Flash */
import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
import _ from 'underscore';
window.Flash = (function() {
var hideFlash;
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
Object.assign(flashEl.style, {
transition: 'opacity .3s',
opacity: '0',
});
}
hideFlash = function() {
return $(this).fadeOut();
};
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
}, {
once: true,
passive: true,
});
/**
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to Parent element under which Flash needs to appear
* @param {Object} actionConfig Map of config to show action on banner
* @param {String} href URL to which action link should point (default '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
*/
function Flash(message, type, parent, actionConfig) {
var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
if (parent == null) {
parent = null;
}
if (parent) {
const $parent = $(parent);
this.flashContainer = $parent.find('.flash-container');
} else {
this.flashContainer = $('.flash-container-page');
}
this.flashContainer.html('');
flash = $('<div/>', {
"class": "flash-" + type
});
flash.on('click', hideFlash);
textDiv = $('<div/>', {
"class": 'flash-text',
text: message
});
textDiv.appendTo(flash);
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
};
if (actionConfig) {
const actionLinkConfig = {
class: 'flash-action',
href: actionConfig.href || '#',
text: actionConfig.title
};
const createAction = config => `
<a
href="${config.href || '#'}"
class="flash-action"
${config.href ? '' : 'role="button"'}
>
${_.escape(config.title)}
</a>
`;
if (!actionConfig.href) {
actionLinkConfig.role = 'button';
}
const createFlashEl = (message, type, isInContentWrapper = false) => `
<div
class="flash-${type}"
>
<div
class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
>
${_.escape(message)}
</div>
</div>
`;
actionLink = $('<a/>', actionLinkConfig);
/*
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message text
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to parent element under which Flash needs to appear
* @param {Object} actonConfig Map of config to show action on banner
* @param {String} href URL to which action config should point to (default: '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
* @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
*/
const createFlash = function createFlash(
message,
type = 'alert',
parent = document,
actionConfig = null,
fadeTransition = true,
) {
const flashContainer = parent.querySelector('.flash-container');
actionLink.appendTo(flash);
this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
}
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
if (!flashContainer) return null;
const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
if (actionConfig) {
flashEl.innerHTML += createAction(actionConfig);
if (actionConfig.clickHandler) {
flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
}
flash.appendTo(this.flashContainer);
this.flashContainer.show();
}
Flash.prototype.destroy = function() {
this.flashContainer.html('');
};
flashContainer.style.display = 'block';
return flashContainer;
};
return Flash;
})();
export {
createFlash as default,
createFlashEl,
createAction,
hideFlash,
};
window.Flash = createFlash;
......@@ -46,6 +46,10 @@ class GeoNodeStatus {
$closestStatus.toggleClass('hidden');
}
static formatCountAndPercentage(count, total, percentage) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`;
}
getStatus() {
$.getJSON(this.endpoint, (status) => {
this.setStatusIcon(status.healthy);
......@@ -62,10 +66,28 @@ class GeoNodeStatus {
this.$dbReplicationLag.text('UNKNOWN');
}
this.$repositoriesSynced.text(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.text(status.repositories_failed_count);
this.$lfsObjectsSynced.text(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$attachmentsSynced.text(`${status.attachments_synced_count}/${status.attachments_count} (${status.attachments_synced_in_percentage})`);
const repoText = GeoNodeStatus.formatCountAndPercentage(
status.repositories_synced_count,
status.repositories_count,
status.repositories_synced_in_percentage);
const repoFailedText = gl.text.addDelimiter(status.repositories_failed_count);
const lfsText = GeoNodeStatus.formatCountAndPercentage(
status.lfs_objects_synced_count,
status.lfs_objects_count,
status.lfs_objects_synced_in_percentage);
const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count,
status.attachments_count,
status.attachments_synced_in_percentage);
this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText);
this.$lfsObjectsSynced.text(lfsText);
this.$attachmentsSynced.text(attachmentText);
const eventDate = gl.utils.formatDate(new Date(status.last_event_date));
const cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_date));
this.$lastEventSeen.text(`${status.last_event_id} (${eventDate})`);
......
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
......
......@@ -2,6 +2,8 @@ document.addEventListener('DOMContentLoaded', () => {
const showGroupLink = () => {
const $cnLink = $('.cn-link');
const $filterLink = $('.filter-link');
if (!$cnLink.length || !$filterLink.length) return;
const $checkedSync = $('input[name="sync_method"]:checked').val() === 'group';
$cnLink.toggle($checkedSync);
......
/* global Flash */
import Flash from '../flash';
export default class IntegrationSettingsForm {
constructor(formSelector) {
......@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
new Flash(`${res.message} ${res.service_response}`, null, null, {
new Flash(`${res.message} ${res.service_response}`, 'alert', document, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
......
<script>
/* global Flash */
/*
`rawReferences` are separated by spaces.
Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']`
......@@ -26,6 +24,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import Flash from '../../../flash';
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
......
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
/* global Flash */
import _ from 'underscore';
import Flash from './flash';
export default {
init({ container, form, issues, prefixId } = {}) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility';
import './flash';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
......@@ -153,7 +152,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error updating issue');
window.Flash('Error updating issue');
});
},
deleteIssuable() {
......@@ -167,7 +166,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error deleting issue');
window.Flash('Error deleting issue');
});
},
},
......
<script>
/* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
......
/* global Flash */
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
......
/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
......
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
/* global Sortable */
import Flash from './flash';
((global) => {
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
......@@ -57,6 +56,7 @@ import './u2f/util';
import './activities';
import './admin';
import './api';
import './ajax_loading_spinner';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
......@@ -76,7 +76,7 @@ import './diff';
import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
import Flash from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
......@@ -178,7 +178,6 @@ $(function () {
var $document = $(document);
var $window = $(window);
var $sidebarGutterToggle = $('.js-sidebar-toggle');
var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize;
......@@ -263,13 +262,6 @@ $(function () {
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Flash
if ($flash.length > 0) {
$flash.click(function () {
return $(this).fadeOut();
});
$flash.show();
}
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
......
/* eslint-disable class-methods-use-this, promise/catch-or-return */
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
(() => {
window.gl = window.gl || {};
......
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global ace */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......
/* eslint-disable new-cap, comma-dangle, no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
......
/* eslint-disable no-new, class-methods-use-this */
/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
import './flash';
import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
/* global Sortable */
import Flash from './flash';
(function() {
this.Milestone = (function() {
function Milestone() {
......
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
......
/* global Flash */
import Flash from '../flash';
import AUTH_METHOD from './constants';
import { backOff } from '../lib/utils/common_utils';
......
<script>
/* global Flash */
import _ from 'underscore';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
......
......@@ -5,7 +5,6 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
/* global Flash */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
......@@ -18,6 +17,7 @@ import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave';
......@@ -354,7 +354,7 @@ export default class Notes {
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
......@@ -593,7 +593,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
}
updateNoteError($parentTimeline) {
......@@ -1213,13 +1213,13 @@ export default class Notes {
}
addFlash(...flashParams) {
this.flashInstance = new Flash(...flashParams);
this.flashContainer = new Flash(...flashParams);
}
clearFlash() {
if (this.flashInstance && this.flashInstance.flashContainer) {
this.flashInstance.flashContainer.hide();
this.flashInstance = null;
if (this.flashContainer) {
this.flashContainer.style.display = 'none';
this.flashContainer = null;
}
}
......
<script>
/* global Flash, Autosave */
/* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
import Flash from '../../flash';
import '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
......@@ -145,7 +146,7 @@
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
$(this.$refs.commentForm),
this.$refs.commentForm,
);
}
} else {
......@@ -160,7 +161,7 @@
this.isSubmitting = false;
this.discard(false);
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
......
<script>
/* global Flash */
import { mapActions, mapGetters } from 'vuex';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
......@@ -133,7 +133,7 @@
this.isReplying = true;
this.$nextTick(() => {
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
});
......
<script>
/* global Flash */
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issueNoteHeader from './issue_note_header.vue';
import issueNoteActions from './issue_note_actions.vue';
......@@ -101,7 +100,7 @@
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
......
<script>
/* global Flash */
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
......
<script>
/* global Flash */
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import issueNote from './issue_note.vue';
......
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
......@@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
Flash('Commands applied', 'notice', $(noteData.flashContainer));
Flash('Commands applied', 'notice', noteData.flashContainer);
}
if (commandsChanges) {
......@@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(() => {
Flash(
'Something went wrong while adding your award. Please try again.',
null,
$(noteData.flashContainer),
'alert',
noteData.flashContainer,
);
});
}
......@@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
if (errors && errors.commands_only) {
Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
Flash(errors.commands_only, 'notice', noteData.flashContainer);
}
commit(types.REMOVE_PLACEHOLDER_NOTES);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
/* global Flash */
import Flash from './flash';
(function() {
this.NotificationsDropdown = (function() {
......
<script>
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
......@@ -13,7 +13,7 @@
* 4. Commit widget
*/
/* global Flash */
import Flash from '../../flash';
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
......
/* global Flash */
import '~/flash';
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
......
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
......
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
......
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
/* global Flash */
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
((global) => {
......
<script>
/* global Flash */
import Flash from '../../../flash';
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
......@@ -44,7 +43,7 @@ export default {
methods: {
fetchIncomingEmail() {
if (this.flash) {
this.flash.destroy();
this.flash.innerHTML = '';
}
this.service.fetchIncomingEmail()
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '../flash';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
......@@ -57,7 +56,7 @@ export default class ProtectedBranchEdit {
},
},
error() {
new Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
},
}).always(() => {
this.$allowedToMergeDropdown.enable();
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '../flash';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
......@@ -43,7 +42,7 @@ export default class ProtectedTagEdit {
},
},
error() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
......
<script>
/* global Flash */
import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
......
/* global Flash */
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import '../../flash';
import Flash from '../../flash';
const RepoHelper = {
monacoInstance: null,
......
/* global Flash */
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
......
/* global Flash */
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Flash */
import Flash from './flash';
import Api from './api';
(function() {
......
/* global Flash */
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
......
<script>
/* global Flash */
import Flash from '../../../flash';
import editForm from './edit_form.vue';
export default {
......
/* global Flash */
function isValidProjectId(id) {
return id > 0;
}
......@@ -38,7 +36,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
.catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
......@@ -73,7 +71,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
Flash('An error occurred while moving the issue.');
window.Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
......
/* global Flash */
import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
import Flash from './flash';
import { __, s__ } from './locale';
export default class Star {
......
/* global Flash */
import 'deckar01-task_list';
import Flash from './flash';
export default class TaskList {
constructor(options = {}) {
......
/* global Flash */
import '~/lib/utils/datetime_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
......
/* global Flash */
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
......
/* global Flash */
import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
......
/* global Flash */
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
......
/* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
......@@ -27,12 +26,12 @@ export default {
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
......
/* global Flash */
import Flash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
......
<script>
/* global Flash */
import Flash from '../../../flash';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
......
......@@ -17,17 +17,11 @@
/* Trial Banner */
.gitlab-ee-license-banner.alert {
display: flex;
align-content: center;
justify-content: center;
padding: 15px 35px;
position: fixed;
top: 0;
width: 100%;
position: relative;
background: $red-400;
color: $white-light;
text-align: center;
z-index: 400;
@media (max-width: $screen-md-min) {
padding: 10px 35px;
......@@ -54,8 +48,8 @@
.close {
position: absolute;
top: 15%;
right: 10px;
top: 12px;
right: $gl-padding;
}
}
......
......@@ -43,6 +43,9 @@ class Groups::LdapGroupLinksController < Groups::ApplicationController
end
def ldap_group_link_params
params.require(:ldap_group_link).permit(:cn, :filter, :group_access, :provider)
attrs = %i[cn group_access provider]
attrs << :filter if ::License.feature_available?(:ldap_group_sync_filter)
params.require(:ldap_group_link).permit(attrs)
end
end
......@@ -17,6 +17,6 @@ class Groups::LdapsController < Groups::ApplicationController
private
def check_enabled_extras!
render_404 unless Gitlab::LDAP::Config.enabled_extras?
render_404 unless Gitlab::LDAP::Config.group_sync_enabled?
end
end
......@@ -230,20 +230,12 @@ class Group < Namespace
ldap_group_links.first.try(:cn)
end
def ldap_filter
ldap_group_links.first.try(:filter)
end
def ldap_access
ldap_group_links.first.try(:group_access)
end
def ldap_cn_or_filter_present?
ldap_cn.present? || ldap_filter.present?
end
def ldap_synced?
Gitlab.config.ldap.enabled && ldap_cn_or_filter_present?
Gitlab.config.ldap.enabled && ldap_group_links.any?(&:active?)
end
def post_create_hook
......
......@@ -43,6 +43,14 @@ class LdapGroupLink < ActiveRecord::Base
config.label
end
def active?
if filter.present?
::License.feature_available?(:ldap_group_sync_filter)
elsif cn.present?
::License.feature_available?(:ldap_group_sync)
end
end
private
def nullify_blank_attributes
......
......@@ -18,10 +18,11 @@ class License < ActiveRecord::Base
issue_board_milestone
issue_weights
jenkins_integration
ldap_extras
ldap_group_sync
merge_request_approvers
merge_request_rebase
merge_request_squash
multiple_ldap_servers
multiple_issue_assignees
multiple_issue_boards
push_rules
......@@ -42,6 +43,7 @@ class License < ActiveRecord::Base
geo
group_issue_boards
jira_dev_panel_integration
ldap_group_sync_filter
object_storage
service_desk
variable_environment_scope
......@@ -108,7 +110,9 @@ class License < ActiveRecord::Base
elastic_search
extended_audit_events
geo
ldap_extras
ldap_group_sync
ldap_group_sync_filter
multiple_ldap_servers
object_storage
repository_size_limit
].freeze
......
......@@ -36,7 +36,7 @@
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
- if ldap_enabled? && @group.persisted?
- if @group.persisted? && Gitlab::LDAP::Config.group_sync_enabled?
%h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group
......@@ -62,11 +62,11 @@
= render partial: "namespaces/shared_runner_status", locals: { namespace: @group }
.panel.panel-default
.panel-heading Active synchronizations
%ul.well-list
- if @group.ldap_group_links.any?
- @group.ldap_group_links.each do |ldap_group_link|
- if Gitlab::LDAP::Config.group_sync_enabled? && @group.ldap_synced?
.panel.panel-default
.panel-heading Active synchronizations
%ul.well-list
- @group.ldap_group_links.select(&:active?).each do |ldap_group_link|
%li
%strong= ldap_group_link.cn ? "Group: #{ldap_group_link.cn}" : "Filter: #{truncate(ldap_group_link.filter, length: 40)}"
as
......
......@@ -3,7 +3,7 @@
The members of this group are managed using LDAP and cannot be added or removed here.
Because LDAP permissions in GitLab get updated one user at a time and because GitLab caches LDAP check results, changes on your LDAP server or in this group's LDAP sync settings may take up to #{Gitlab.config.ldap['sync_time']}s to show in the list below.
%ul
- @group.ldap_group_links.each do |ldap_group_link|
- @group.ldap_group_links.select(&:active?).each do |ldap_group_link|
%li
People in
%code= ldap_group_link.cn ? "cn: #{ldap_group_link.cn}" : "filter: #{truncate(ldap_group_link.filter, length: 70)}"
......
- page_title 'LDAP Syncrhonizations'
- page_title 'LDAP Synchronization'
%h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group
......@@ -4,6 +4,7 @@
.content-wrapper.page-with-new-nav
.mobile-overlay
.alert-wrapper
= render "layouts/header/ee_license_banner"
= render "layouts/broadcast"
= yield :flash_message
- unless @hide_breadcrumbs
......
= render "layouts/header/ee_license_banner"
%header.navbar.navbar-gitlab
%header.navbar.navbar-gitlab.navbar-gitlab-new
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
......
......@@ -9,18 +9,19 @@
.col-sm-10
= f.select :provider, ldap_server_select_options, {}, class: 'form-control'
.form-group.row
= f.label :cn, class: 'control-label col-sm-2' do
Sync method
.col-sm-10
.radio
= label_tag :sync_method_group do
= radio_button_tag :sync_method, :group, true
LDAP Group cn
.radio
= label_tag :sync_method_filter do
= radio_button_tag :sync_method, :filter
LDAP user filter
- if ::License.feature_available?(:ldap_group_sync_filter)
.form-group.row
= f.label :cn, class: 'control-label col-sm-2' do
Sync method
.col-sm-10
.radio
= label_tag :sync_method_group do
= radio_button_tag :sync_method, :group, true
LDAP Group cn
.radio
= label_tag :sync_method_filter do
= radio_button_tag :sync_method, :filter
LDAP user filter
.form-group.row.cn-link
= f.label :cn, class: 'control-label col-sm-2' do
......@@ -32,16 +33,17 @@
%br
If you select an LDAP group you do not belong to you will lose ownership of #{group.name}.
.form-group.row.filter-link
= f.label :filter, class: 'control-label col-sm-2' do
LDAP User filter
.col-sm-10
= f.text_field :filter, placeholder: 'Ex. (&(objectCategory=person)(objectClass=developer))', class: 'form-control xxlarge input-mn-300'
.help-block
- ldap_link = link_to 'LDAP Search Filter Syntax', 'https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx'
This query must use valid #{ldap_link}. Synchronize #{group.name}'s members with this LDAP user filter.
%br
If you do not belong to this LDAP user filter you will lose ownership of #{group.name}.
- if ::License.feature_available?(:ldap_group_sync_filter)
.form-group.row.filter-link
= f.label :filter, class: 'control-label col-sm-2' do
LDAP User filter
.col-sm-10
= f.text_field :filter, placeholder: 'Ex. (&(objectCategory=person)(objectClass=developer))', class: 'form-control xxlarge input-mn-300'
.help-block
- ldap_link = link_to 'LDAP Search Filter Syntax', 'https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx'
This query must use valid #{ldap_link}. Synchronize #{group.name}'s members with this LDAP user filter.
%br
If you do not belong to this LDAP user filter you will lose ownership of #{group.name}.
.form-group.row
= f.label :group_access, class: 'control-label col-sm-2' do
......
......@@ -13,3 +13,6 @@
Config for LDAP server
%code= ldap_group_link.provider
is not present in GitLab
- unless ldap_group_link.active?
(Inactive because syncing with an LDAP user filter is not included in the current license)
......@@ -3,7 +3,7 @@ class LdapAllGroupsSyncWorker
include CronjobQueue
def perform
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
logger.info 'Started LDAP group sync'
EE::Gitlab::LDAP::Sync::Groups.execute
......
......@@ -3,7 +3,7 @@ class LdapGroupSyncWorker
include DedicatedSidekiqQueue
def perform(group_ids, provider = nil)
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
groups = Group.where(id: Array(group_ids))
......
......@@ -3,7 +3,7 @@ class LdapSyncWorker
include CronjobQueue
def perform
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
Rails.logger.info "Performing daily LDAP sync task."
User.ldap.find_each(batch_size: 100).each do |ldap_user|
......
---
title: Fix base link for issues on group boards
merge_request:
author:
type: fixed
......@@ -20,7 +20,7 @@ membership syncing.
## Use-cases
- User Sync: Once a day, GitLab will update users against LDAP
- Group Sync: Once an hour, GitLab will update group membership
- Group Sync: Once an hour, GitLab will update group membership
based on LDAP group members
## User Sync
......@@ -54,8 +54,8 @@ new groups they might be added to when the user logs in. That way they don't nee
to wait for the hourly sync to be granted access to the groups that they are in
in LDAP.
We can also add a GitLab group to sync with one or multiple LDAP groups or we can
also add a filter. The filter must comply with the syntax defined in [RFC 2254](https://tools.ietf.org/search/rfc2254).
In GitLab Enterprise Edition Premium, we can also add a GitLab group to sync with one or multiple LDAP groups or we can
also add a filter. The filter must comply with the syntax defined in [RFC 2254](https://tools.ietf.org/search/rfc2254).
A group sync process will run every hour on the hour, and `group_base` must be set
in LDAP configuration for LDAP synchronizations based on group CN to work. This allows
......
# Development
# GitLab development guides
## Outside of docs
## Get started!
- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide
- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process
- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version
## Styleguides
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
contributing to documentation.
- [Writing documentation](writing_documentation.md)
- [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [`Gemfile` guidelines](gemfile.md)
- Setup GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
- [GitLab contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
- [Architecture](architecture.md) of GitLab
- [Rake tasks](rake_tasks.md) for development
## Process
## Processes
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
- [Guidelines for implementing Enterprise Edition feature](ee_features.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
## Backend howtos
## UX and frontend guides
- [Architecture](architecture.md) of GitLab
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
## Backend guides
- [Testing standards and style guidelines](testing_guide/index.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags.md)
- [View sent emails or preview mailers](emails.md)
- [Shell commands](shell_commands.md) in the GitLab codebase
- [`Gemfile` guidelines](gemfile.md)
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
## Performance guides
- [Instrumentation](instrumentation.md)
- [Performance guidelines](performance.md)
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
- [Object state models](object_state_models.md)
- [Building a package for testing purposes](build_test_package.md)
- [Manage feature flags](feature_flags.md)
- [View sent emails or preview mailers](emails.md)
- [Working with Gitaly](gitaly.md)
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
## Databases
## Databases guides
### Migrations
- [Merge Request Checklist](database_merge_request_checklist.md)
- [What requires downtime?](what_requires_downtime.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Migrations style guide](migration_style_guide.md) for creating safe SQL migrations
- [Post deployment migrations](post_deployment_migrations.md)
- [Background migrations](background_migrations.md)
- [Swapping tables](swapping_tables.md)
### Best practices
- [Merge Request checklist](database_merge_request_checklist.md)
- [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign Keys & Associations](foreign_keys.md)
- [Serializing Data](serializing_data.md)
- [Polymorphic Associations](polymorphic_associations.md)
- [Single Table Inheritance](single_table_inheritance.md)
- [Foreign keys & associations](foreign_keys.md)
- [Serializing data](serializing_data.md)
- [Polymorphic associations](polymorphic_associations.md)
- [Single table inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
- [Iterating Tables In Batches](iterating_tables_in_batches.md)
- [Ordering Table Columns](ordering_table_columns.md)
- [Verifying Database Capabilities](verifying_database_capabilities.md)
- [Hash Indexes](hash_indexes.md)
- [Storing SHA1 hashes as binary](sha1_as_binary.md)
- [Iterating tables in batches](iterating_tables_in_batches.md)
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Hash indexes](hash_indexes.md)
- [Swapping Tables](swapping_tables.md)
## Internationalization (i18n)
## Documentation guides
- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
contributing to the documentation.
- [Writing documentation](writing_documentation.md)
- [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
## Internationalization (i18n) guides
- [Introduction](i18n/index.md)
- [Externalization](i18n/externalization.md)
- [Translation](i18n/translation.md)
## Build guides
- [Building a package for testing purposes](build_test_package.md)
## Compliance
- [Licensing](licensing.md) for ensuring license compliance
......@@ -54,7 +54,8 @@ or make changes to our frontend development guidelines.
---
## [Testing](testing.md)
## [Testing](../testing_guide/frontend_testing.md)
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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