Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
215cb09934
commit
77cf68da37
|
@ -7,7 +7,6 @@
|
|||
before_script:
|
||||
- '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb'
|
||||
- cd qa/
|
||||
- gem install bundler -v 1.17.3
|
||||
- bundle install --clean --jobs=$(nproc) --path=vendor --retry=3 --without=development --quiet
|
||||
- bundle check
|
||||
|
||||
|
|
|
@ -183,7 +183,6 @@ update-coverage-cache:
|
|||
- .shared:rules:update-cache
|
||||
stage: prepare
|
||||
script:
|
||||
- run_timed_command "gem install bundler -v 1.17.3"
|
||||
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
|
||||
cache:
|
||||
policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
|
||||
|
@ -289,6 +288,7 @@ db:migrate-from-v12.10.0:
|
|||
- git checkout -f FETCH_HEAD
|
||||
- sed -i -e "s/gem 'grpc', '~> 1.24.0'/gem 'grpc', '~> 1.30.2'/" Gemfile # Update gRPC for Ruby 2.7
|
||||
- sed -i -e "s/gem 'google-protobuf', '~> 3.8.0'/gem 'google-protobuf', '~> 3.12.0'/" Gemfile
|
||||
- gem install bundler:1.17.3
|
||||
- bundle update google-protobuf grpc bootsnap
|
||||
- bundle install $BUNDLE_INSTALL_FLAGS
|
||||
- date
|
||||
|
@ -363,7 +363,6 @@ rspec:coverage:
|
|||
- memory-static
|
||||
- memory-on-boot
|
||||
script:
|
||||
- run_timed_command "gem install bundler -v 1.17.3"
|
||||
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
|
||||
- run_timed_command "bundle exec scripts/merge-simplecov"
|
||||
- run_timed_command "bundle exec scripts/gather-test-memory-data"
|
||||
|
@ -401,7 +400,6 @@ rspec:feature-flags:
|
|||
- memory-static
|
||||
- memory-on-boot
|
||||
script:
|
||||
- run_timed_command "gem install bundler -v 1.17.3"
|
||||
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
|
||||
- run_timed_command "bundle exec scripts/used-feature-flags"
|
||||
# EE/FOSS: default refs (MRs, master, schedules) jobs #
|
||||
|
|
|
@ -39,7 +39,7 @@ update-tests-metadata:
|
|||
- rspec-ee integration pg11 geo
|
||||
- rspec-ee system pg11 geo
|
||||
script:
|
||||
- run_timed_command "retry gem install bundler:1.17.3 fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
|
||||
- run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
|
||||
- source ./scripts/rspec_helpers.sh
|
||||
- update_tests_metadata
|
||||
- update_tests_mapping
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -119,7 +119,7 @@ gem 'fog-aws', '~> 3.5'
|
|||
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
|
||||
# Also see config/initializers/fog_core_patch.rb.
|
||||
gem 'fog-core', '= 2.1.0'
|
||||
gem 'fog-google', '~> 1.10'
|
||||
gem 'fog-google', '~> 1.11'
|
||||
gem 'fog-local', '~> 0.6'
|
||||
gem 'fog-openstack', '~> 1.0'
|
||||
gem 'fog-rackspace', '~> 0.1.1'
|
||||
|
|
|
@ -369,11 +369,12 @@ GEM
|
|||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
mime-types
|
||||
fog-google (1.10.0)
|
||||
fog-google (1.11.0)
|
||||
fog-core (<= 2.1.0)
|
||||
fog-json (~> 1.2)
|
||||
fog-xml (~> 0.1.0)
|
||||
google-api-client (>= 0.32, < 0.34)
|
||||
google-cloud-env (~> 1.2)
|
||||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
|
@ -475,6 +476,8 @@ GEM
|
|||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-env (1.4.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-protobuf (3.12.4)
|
||||
googleapis-common-protos-types (1.0.5)
|
||||
google-protobuf (~> 3.11)
|
||||
|
@ -1325,7 +1328,7 @@ DEPENDENCIES
|
|||
fog-aliyun (~> 0.3)
|
||||
fog-aws (~> 3.5)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.10)
|
||||
fog-google (~> 1.11)
|
||||
fog-local (~> 0.6)
|
||||
fog-openstack (~> 1.0)
|
||||
fog-rackspace (~> 0.1.1)
|
||||
|
@ -1521,4 +1524,4 @@ DEPENDENCIES
|
|||
yajl-ruby (~> 1.4.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
2.1.4
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { sprintf, n__, __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
addedFilesLength() {
|
||||
return this.files.filter(f => f.tempFile).length;
|
||||
},
|
||||
modifiedFilesLength() {
|
||||
return this.files.filter(f => !f.tempFile).length;
|
||||
},
|
||||
addedFilesIconClass() {
|
||||
return this.addedFilesLength ? 'multi-file-addition' : '';
|
||||
},
|
||||
modifiedFilesClass() {
|
||||
return this.modifiedFilesLength ? 'multi-file-modified' : '';
|
||||
},
|
||||
additionsTooltip() {
|
||||
return sprintf(
|
||||
n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength),
|
||||
{
|
||||
type: this.title.toLowerCase(),
|
||||
count: this.addedFilesLength,
|
||||
},
|
||||
);
|
||||
},
|
||||
modifiedTooltip() {
|
||||
return sprintf(
|
||||
n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength),
|
||||
{
|
||||
type: this.title.toLowerCase(),
|
||||
count: this.modifiedFilesLength,
|
||||
},
|
||||
);
|
||||
},
|
||||
titleTooltip() {
|
||||
return sprintf(__('%{title} changes'), { title: this.title });
|
||||
},
|
||||
additionIconName() {
|
||||
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
|
||||
},
|
||||
modifiedIconName() {
|
||||
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list-collapsed text-center">
|
||||
<div
|
||||
v-tooltip
|
||||
:title="titleTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="gl-mb-5"
|
||||
>
|
||||
<gl-icon v-once :name="iconName" :size="18" />
|
||||
</div>
|
||||
<div
|
||||
v-tooltip
|
||||
:title="additionsTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="gl-mb-3"
|
||||
>
|
||||
<gl-icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
|
||||
</div>
|
||||
{{ addedFilesLength }}
|
||||
<div
|
||||
v-tooltip
|
||||
:title="modifiedTooltip"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
class="gl-mt-3 gl-mb-3"
|
||||
>
|
||||
<gl-icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
|
||||
</div>
|
||||
{{ modifiedFilesLength }}
|
||||
</div>
|
||||
</template>
|
|
@ -14,6 +14,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters(['activeFile']),
|
||||
...mapGetters('editor', ['activeFileEditor']),
|
||||
activeFileEOL() {
|
||||
return getFileEOL(this.activeFile.content);
|
||||
},
|
||||
|
@ -33,8 +34,10 @@ export default {
|
|||
</gl-link>
|
||||
</div>
|
||||
<div>{{ activeFileEOL }}</div>
|
||||
<div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
|
||||
<div>{{ activeFile.fileLanguage }}</div>
|
||||
<div v-if="activeFileIsText">
|
||||
{{ activeFileEditor.editorRow }}:{{ activeFileEditor.editorColumn }}
|
||||
</div>
|
||||
<div>{{ activeFileEditor.fileLanguage }}</div>
|
||||
</template>
|
||||
<terminal-sync-status-safe />
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@ import Editor from '../lib/editor';
|
|||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { extractMarkdownImagesFromEntries } from '../stores/utils';
|
||||
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
|
||||
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
|
||||
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
|
||||
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
|
||||
|
@ -49,6 +50,7 @@ export default {
|
|||
...mapState('rightPane', {
|
||||
rightPaneIsOpen: 'isOpen',
|
||||
}),
|
||||
...mapState('editor', ['fileEditors']),
|
||||
...mapState([
|
||||
'viewer',
|
||||
'panelResizing',
|
||||
|
@ -67,6 +69,9 @@ export default {
|
|||
'getJsonSchemaForPath',
|
||||
]),
|
||||
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||
fileEditor() {
|
||||
return getFileEditorOrDefault(this.fileEditors, this.file.path);
|
||||
},
|
||||
shouldHideEditor() {
|
||||
return this.file && !this.file.loading && !isTextFile(this.file);
|
||||
},
|
||||
|
@ -80,10 +85,10 @@ export default {
|
|||
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
|
||||
},
|
||||
isEditorViewMode() {
|
||||
return this.file.viewMode === FILE_VIEW_MODE_EDITOR;
|
||||
return this.fileEditor.viewMode === FILE_VIEW_MODE_EDITOR;
|
||||
},
|
||||
isPreviewViewMode() {
|
||||
return this.file.viewMode === FILE_VIEW_MODE_PREVIEW;
|
||||
return this.fileEditor.viewMode === FILE_VIEW_MODE_PREVIEW;
|
||||
},
|
||||
editTabCSS() {
|
||||
return {
|
||||
|
@ -125,8 +130,7 @@ export default {
|
|||
this.initEditor();
|
||||
|
||||
if (this.currentActivityView !== leftSidebarViews.edit.name) {
|
||||
this.setFileViewMode({
|
||||
file: this.file,
|
||||
this.updateEditor({
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
});
|
||||
}
|
||||
|
@ -134,8 +138,7 @@ export default {
|
|||
},
|
||||
currentActivityView() {
|
||||
if (this.currentActivityView !== leftSidebarViews.edit.name) {
|
||||
this.setFileViewMode({
|
||||
file: this.file,
|
||||
this.updateEditor({
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
});
|
||||
}
|
||||
|
@ -195,13 +198,11 @@ export default {
|
|||
'getFileData',
|
||||
'getRawFileData',
|
||||
'changeFileContent',
|
||||
'setFileLanguage',
|
||||
'setEditorPosition',
|
||||
'setFileViewMode',
|
||||
'removePendingTab',
|
||||
'triggerFilesChange',
|
||||
'addTempImage',
|
||||
]),
|
||||
...mapActions('editor', ['updateFileEditor']),
|
||||
initEditor() {
|
||||
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
|
||||
return;
|
||||
|
@ -284,19 +285,19 @@ export default {
|
|||
|
||||
// Handle Cursor Position
|
||||
this.editor.onPositionChange((instance, e) => {
|
||||
this.setEditorPosition({
|
||||
this.updateEditor({
|
||||
editorRow: e.position.lineNumber,
|
||||
editorColumn: e.position.column,
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.setPosition({
|
||||
lineNumber: this.file.editorRow,
|
||||
column: this.file.editorColumn,
|
||||
lineNumber: this.fileEditor.editorRow,
|
||||
column: this.fileEditor.editorColumn,
|
||||
});
|
||||
|
||||
// Handle File Language
|
||||
this.setFileLanguage({
|
||||
this.updateEditor({
|
||||
fileLanguage: this.model.language,
|
||||
});
|
||||
|
||||
|
@ -354,6 +355,16 @@ export default {
|
|||
const schema = this.getJsonSchemaForPath(this.file.path);
|
||||
registerSchema(schema);
|
||||
},
|
||||
updateEditor(data) {
|
||||
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
|
||||
// when disposing. We want to ignore these by only capturing editor changes that happen to the currently active
|
||||
// file.
|
||||
if (!this.file.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFileEditor({ path: this.file.path, data });
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
FILE_VIEW_MODE_EDITOR,
|
||||
|
@ -369,7 +380,7 @@ export default {
|
|||
<a
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_EDITOR })"
|
||||
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
</a>
|
||||
|
@ -378,7 +389,7 @@ export default {
|
|||
<a
|
||||
href="javascript:void(0);"
|
||||
role="button"
|
||||
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
|
||||
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
|
||||
>{{ previewMode.previewTitle }}</a
|
||||
>
|
||||
</li>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
|
|||
import { deprecatedCreateFlash as flash } from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import { decorateFiles } from '../lib/files';
|
||||
import { stageKeys } from '../constants';
|
||||
import { stageKeys, commitActionTypes } from '../constants';
|
||||
import service from '../services';
|
||||
import eventHub from '../eventhub';
|
||||
|
||||
|
@ -242,7 +242,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
|
|||
}
|
||||
}
|
||||
|
||||
dispatch('triggerFilesChange');
|
||||
dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath });
|
||||
};
|
||||
|
||||
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
|
||||
|
|
|
@ -164,26 +164,6 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
|
|||
}
|
||||
};
|
||||
|
||||
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
|
||||
}
|
||||
};
|
||||
|
||||
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_POSITION, {
|
||||
file: getters.activeFile,
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setFileViewMode = ({ commit }, { file, viewMode }) => {
|
||||
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
|
||||
};
|
||||
|
||||
export const restoreOriginalFile = ({ dispatch, state, commit }, path) => {
|
||||
const file = state.entries[path];
|
||||
const isDestructiveDiscard = file.tempFile || file.prevPath;
|
||||
|
@ -289,7 +269,7 @@ export const removePendingTab = ({ commit }, file) => {
|
|||
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
|
||||
};
|
||||
|
||||
export const triggerFilesChange = () => {
|
||||
export const triggerFilesChange = (ctx, payload = {}) => {
|
||||
// Used in EE for file mirroring
|
||||
eventHub.$emit('ide.files.change');
|
||||
eventHub.$emit('ide.files.change', payload);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,8 @@ import fileTemplates from './modules/file_templates';
|
|||
import paneModule from './modules/pane';
|
||||
import clientsideModule from './modules/clientside';
|
||||
import routerModule from './modules/router';
|
||||
import editorModule from './modules/editor';
|
||||
import { setupFileEditorsSync } from './modules/editor/setup';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -29,7 +31,14 @@ export const createStoreOptions = () => ({
|
|||
rightPane: paneModule(),
|
||||
clientside: clientsideModule(),
|
||||
router: routerModule,
|
||||
editor: editorModule,
|
||||
},
|
||||
});
|
||||
|
||||
export const createStore = () => new Vuex.Store(createStoreOptions());
|
||||
export const createStore = () => {
|
||||
const store = new Vuex.Store(createStoreOptions());
|
||||
|
||||
setupFileEditorsSync(store);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
/**
|
||||
* Action to update the current file editor info at the given `path` with the given `data`
|
||||
*
|
||||
* @param {} vuex
|
||||
* @param {{ path: String, data: any }} payload
|
||||
*/
|
||||
export const updateFileEditor = ({ commit }, payload) => {
|
||||
commit(types.UPDATE_FILE_EDITOR, payload);
|
||||
};
|
||||
|
||||
export const removeFileEditor = ({ commit }, path) => {
|
||||
commit(types.REMOVE_FILE_EDITOR, path);
|
||||
};
|
||||
|
||||
export const renameFileEditor = ({ commit }, payload) => {
|
||||
commit(types.RENAME_FILE_EDITOR, payload);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { getFileEditorOrDefault } from './utils';
|
||||
|
||||
export const activeFileEditor = (state, getters, rootState, rootGetters) => {
|
||||
const { activeFile } = rootGetters;
|
||||
|
||||
if (!activeFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { path } = rootGetters.activeFile;
|
||||
|
||||
return getFileEditorOrDefault(state.fileEditors, path);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import state from './state';
|
||||
import mutations from './mutations';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
actions,
|
||||
state,
|
||||
mutations,
|
||||
getters,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export const UPDATE_FILE_EDITOR = 'UPDATE_FILE_EDITOR';
|
||||
export const REMOVE_FILE_EDITOR = 'REMOVE_FILE_EDITOR';
|
||||
export const RENAME_FILE_EDITOR = 'RENAME_FILE_EDITOR';
|
|
@ -0,0 +1,25 @@
|
|||
import Vue from 'vue';
|
||||
import * as types from './mutation_types';
|
||||
import { getFileEditorOrDefault } from './utils';
|
||||
|
||||
export default {
|
||||
[types.UPDATE_FILE_EDITOR](state, { path, data }) {
|
||||
const editor = getFileEditorOrDefault(state.fileEditors, path);
|
||||
|
||||
Vue.set(state.fileEditors, path, Object.assign(editor, data));
|
||||
},
|
||||
[types.REMOVE_FILE_EDITOR](state, path) {
|
||||
Vue.delete(state.fileEditors, path);
|
||||
},
|
||||
[types.RENAME_FILE_EDITOR](state, { path, newPath }) {
|
||||
const existing = state.fileEditors[path];
|
||||
|
||||
// Gracefully do nothing if fileEditor isn't found.
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.delete(state.fileEditors, path);
|
||||
Vue.set(state.fileEditors, newPath, existing);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import eventHub from '~/ide/eventhub';
|
||||
import { commitActionTypes } from '~/ide/constants';
|
||||
|
||||
const removeUnusedFileEditors = store => {
|
||||
Object.keys(store.state.editor.fileEditors)
|
||||
.filter(path => !store.state.entries[path])
|
||||
.forEach(path => store.dispatch('editor/removeFileEditor', path));
|
||||
};
|
||||
|
||||
export const setupFileEditorsSync = store => {
|
||||
eventHub.$on('ide.files.change', ({ type, ...payload } = {}) => {
|
||||
if (type === commitActionTypes.move) {
|
||||
store.dispatch('editor/renameFileEditor', payload);
|
||||
} else {
|
||||
// The files have changed, but the specific change is not known.
|
||||
removeUnusedFileEditors(store);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
export default () => ({
|
||||
// Object which represents a dictionary of filePath to editor specific properties, including:
|
||||
// - fileLanguage
|
||||
// - editorRow
|
||||
// - editorCol
|
||||
// - viewMode
|
||||
fileEditors: {},
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { FILE_VIEW_MODE_EDITOR } from '../../../constants';
|
||||
|
||||
export const createDefaultFileEditor = () => ({
|
||||
editorRow: 1,
|
||||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
});
|
||||
|
||||
export const getFileEditorOrDefault = (fileEditors, path) =>
|
||||
fileEditors[path] || createDefaultFileEditor();
|
|
@ -36,9 +36,6 @@ export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
|||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
|
||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
||||
export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
|
||||
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
|
||||
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
|
||||
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
|
||||
|
|
|
@ -95,17 +95,6 @@ export default {
|
|||
changed,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
fileLanguage,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
|
||||
let diffMode = diffModes.replaced;
|
||||
if (mrChange.new_file) {
|
||||
|
@ -122,11 +111,6 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
viewMode,
|
||||
});
|
||||
},
|
||||
[types.DISCARD_FILE_CHANGES](state, path) {
|
||||
const stagedFile = state.stagedFiles.find(f => f.path === path);
|
||||
const entry = state.entries[path];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants';
|
||||
import { commitActionTypes } from '../constants';
|
||||
import {
|
||||
relativePathToAbsolute,
|
||||
isAbsolute,
|
||||
|
@ -25,10 +25,6 @@ export const dataStructure = () => ({
|
|||
rawPath: '',
|
||||
raw: '',
|
||||
content: '',
|
||||
editorRow: 1,
|
||||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
size: 0,
|
||||
parentPath: null,
|
||||
lastOpenedAt: 0,
|
||||
|
|
|
@ -116,7 +116,8 @@ export default {
|
|||
<div data-testid="issuable-title" class="issue-title title">
|
||||
<span class="issue-title-text" dir="auto">
|
||||
<gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
|
||||
>{{ issuable.title }}<gl-icon v-if="isIssuableUrlExternal" name="external-link"
|
||||
>{{ issuable.title
|
||||
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
|
||||
/></gl-link>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -134,7 +135,9 @@ export default {
|
|||
>{{ createdAt }}</span
|
||||
>
|
||||
{{ __('by') }}
|
||||
<slot v-if="hasSlotContents('author')" name="author"></slot>
|
||||
<gl-link
|
||||
v-else
|
||||
:data-user-id="authorId"
|
||||
:data-username="author.username"
|
||||
:data-name="author.name"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
|
@ -7,9 +7,11 @@ import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filte
|
|||
import IssuableTabs from './issuable_tabs.vue';
|
||||
import IssuableItem from './issuable_item.vue';
|
||||
|
||||
import { DEFAULT_SKELETON_COUNT } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlSkeletonLoading,
|
||||
IssuableTabs,
|
||||
FilteredSearchBar,
|
||||
IssuableItem,
|
||||
|
@ -88,7 +90,7 @@ export default {
|
|||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
totalPages: {
|
||||
totalItems: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
|
@ -114,6 +116,19 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
skeletonItemCount() {
|
||||
const { totalItems, defaultPageSize, currentPage } = this;
|
||||
const totalPages = Math.ceil(totalItems / defaultPageSize);
|
||||
|
||||
if (totalPages) {
|
||||
return currentPage < totalPages
|
||||
? defaultPageSize
|
||||
: totalItems % defaultPageSize || defaultPageSize;
|
||||
}
|
||||
return DEFAULT_SKELETON_COUNT;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
urlParams: {
|
||||
deep: true,
|
||||
|
@ -157,7 +172,11 @@ export default {
|
|||
@onSort="$emit('sort', $event)"
|
||||
/>
|
||||
<div class="issuables-holder">
|
||||
<gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" />
|
||||
<ul v-if="issuablesLoading" class="content-list">
|
||||
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
|
||||
<gl-skeleton-loading />
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
v-if="!issuablesLoading && issuables.length"
|
||||
class="content-list issuable-list issues-list"
|
||||
|
@ -172,6 +191,9 @@ export default {
|
|||
<template #reference>
|
||||
<slot name="reference" :issuable="issuable"></slot>
|
||||
</template>
|
||||
<template #author>
|
||||
<slot name="author" :author="issuable.author"></slot>
|
||||
</template>
|
||||
<template #status>
|
||||
<slot name="status" :issuable="issuable"></slot>
|
||||
</template>
|
||||
|
@ -181,7 +203,7 @@ export default {
|
|||
<gl-pagination
|
||||
v-if="showPaginationControls"
|
||||
:per-page="defaultPageSize"
|
||||
:total-items="totalPages"
|
||||
:total-items="totalItems"
|
||||
:value="currentPage"
|
||||
:prev-page="previousPage"
|
||||
:next-page="nextPage"
|
||||
|
|
|
@ -47,3 +47,5 @@ export const AvailableSortOptions = [
|
|||
];
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export const DEFAULT_SKELETON_COUNT = 5;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import initIssuablesList from '~/issues_list';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initIssuablesList();
|
||||
});
|
|
@ -1,8 +1,21 @@
|
|||
<script>
|
||||
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
|
||||
import {
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
|
@ -17,6 +30,24 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
templates: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
currentTemplate: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownLabel() {
|
||||
return this.currentTemplate ? this.currentTemplate.name : __('None');
|
||||
},
|
||||
hasTemplates() {
|
||||
return this.templates?.length > 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.preSelect();
|
||||
|
@ -30,6 +61,9 @@ export default {
|
|||
this.$refs.title.$el.select();
|
||||
});
|
||||
},
|
||||
onChangeTemplate(template) {
|
||||
this.$emit('changeTemplate', template || null);
|
||||
},
|
||||
onUpdate(field, value) {
|
||||
const payload = {
|
||||
title: this.title,
|
||||
|
@ -58,6 +92,29 @@ export default {
|
|||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="hasTemplates"
|
||||
key="template"
|
||||
:label="__('Description template')"
|
||||
:label-for="getId('control', 'template')"
|
||||
>
|
||||
<gl-dropdown :text="dropdownLabel">
|
||||
<gl-dropdown-item key="none" @click="onChangeTemplate(null)">
|
||||
{{ __('None') }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-dropdown-item
|
||||
v-for="template in templates"
|
||||
:key="template.key"
|
||||
@click="onChangeTemplate(template)"
|
||||
>
|
||||
{{ template.name }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
key="description"
|
||||
:label="__('Goal of the changes and what reviewers should be aware of')"
|
||||
|
|
|
@ -22,6 +22,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
clearStorage: false,
|
||||
currentTemplate: null,
|
||||
mergeRequestTemplates: null,
|
||||
mergeRequestMeta: {
|
||||
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
|
||||
sourcePath: this.sourcePath,
|
||||
|
@ -61,6 +63,13 @@ export default {
|
|||
onSecondary() {
|
||||
this.hide();
|
||||
},
|
||||
onChangeTemplate(template) {
|
||||
this.currentTemplate = template;
|
||||
|
||||
const description = this.currentTemplate ? this.currentTemplate.content : '';
|
||||
const mergeRequestMeta = { ...this.mergeRequestMeta, description };
|
||||
this.onUpdateSettings(mergeRequestMeta);
|
||||
},
|
||||
onUpdateSettings(mergeRequestMeta) {
|
||||
this.mergeRequestMeta = { ...mergeRequestMeta };
|
||||
},
|
||||
|
@ -91,7 +100,10 @@ export default {
|
|||
ref="editMetaControls"
|
||||
:title="mergeRequestMeta.title"
|
||||
:description="mergeRequestMeta.description"
|
||||
:templates="mergeRequestTemplates"
|
||||
:current-template="currentTemplate"
|
||||
@updateSettings="onUpdateSettings"
|
||||
@changeTemplate="onChangeTemplate"
|
||||
/>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
@ -60,8 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
def update_registration
|
||||
return redirect_to new_user_registration_path unless current_user
|
||||
|
||||
user_params = params.require(:user).permit(:role, :setup_for_company)
|
||||
result = ::Users::SignupService.new(current_user, user_params).execute
|
||||
result = ::Users::SignupService.new(current_user, update_registration_params).execute
|
||||
|
||||
if result[:status] == :success
|
||||
if ::Gitlab.com? && show_onboarding_issues_experiment?
|
||||
|
@ -164,6 +163,10 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
params.require(:user).permit(:username, :email, :name, :first_name, :last_name, :password)
|
||||
end
|
||||
|
||||
def update_registration_params
|
||||
params.require(:user).permit(:role, :setup_for_company)
|
||||
end
|
||||
|
||||
def resource_name
|
||||
:user
|
||||
end
|
||||
|
|
|
@ -153,10 +153,8 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def row_count
|
||||
fast_fail = Feature.enabled?(:soft_fail_count_by_state, params.group || params.project)
|
||||
|
||||
Gitlab::IssuablesCountForState
|
||||
.new(self, nil, fast_fail: fast_fail)
|
||||
.new(self, nil, fast_fail: true)
|
||||
.for_state_or_opened(params[:state])
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
= f.label :password, class: 'label-bold'
|
||||
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
|
||||
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
|
||||
= render_if_exists 'devise/shared/email_opted_in', f: f
|
||||
%div
|
||||
- if show_recaptcha_sign_up?
|
||||
= recaptcha_tags
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
= f.label :password, class: 'label-bold'
|
||||
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
|
||||
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
|
||||
= render_if_exists 'devise/shared/email_opted_in', f: f
|
||||
%div
|
||||
- if show_recaptcha_sign_up?
|
||||
= recaptcha_tags
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Gracefully degrade when counting takes too long for a filtered search
|
||||
merge_request: 46350
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Upgrade fog-google to v1.11.0
|
||||
merge_request: 46648
|
||||
author:
|
||||
type: fixed
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: soft_fail_count_by_state
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44184
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263222
|
||||
name: jira_issues_list
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45678
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273726
|
||||
type: development
|
||||
group: group::source code
|
||||
group: group::ecosystem
|
||||
default_enabled: false
|
|
@ -6,7 +6,7 @@
|
|||
gitlab-com: true
|
||||
packages: [Starter, Premium, Ultimate]
|
||||
url: https://www.youtube.com/watch?v=31pNKjenlJY&feature=emb_title
|
||||
image_url: http://i3.ytimg.com/vi/31pNKjenlJY/maxresdefault.jpg
|
||||
image_url: https://img.youtube.com/vi/31pNKjenlJY/hqdefault.jpg
|
||||
published_at: 2020-07-22
|
||||
release: 13.2
|
||||
- title: Container Host Monitoring and Blocking
|
||||
|
@ -16,7 +16,7 @@
|
|||
gitlab-com: true
|
||||
packages: [All]
|
||||
url: https://www.youtube.com/watch?v=WxBzBz76FxU&feature=youtu.be
|
||||
image_url: http://i3.ytimg.com/vi/WxBzBz76FxU/hqdefault.jpg
|
||||
image_url: https://img.youtube.com/vi/WxBzBz76FxU/hqdefault.jpg
|
||||
published_at: 2020-07-22
|
||||
release: 13.2
|
||||
- title: Official GitLab-Figma Plugin
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
gitlab-com: true
|
||||
packages: [Ultimate]
|
||||
url: https://www.youtube.com/watch?v=3wdWMDRLdp4
|
||||
image_url: http://i3.ytimg.com/vi/3wdWMDRLdp4/hqdefault.jpg
|
||||
image_url: https://img.youtube.com/vi/3wdWMDRLdp4/hqdefault.jpg
|
||||
published_at: 2020-08-22
|
||||
release: 13.3
|
||||
- title: Create a matrix of jobs using a simple syntax
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
gitlab-com: true
|
||||
packages: [starter, premium, ultimate]
|
||||
url: https://www.youtube.com/embed/1FBRaBQTQZk
|
||||
image_url: http://i3.ytimg.com/vi/1FBRaBQTQZk/maxresdefault.jpg
|
||||
image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg
|
||||
published_at: 2020-09-22
|
||||
release: 13.4
|
||||
- title: Quick navigation using the Search bar
|
||||
|
|
|
@ -124,8 +124,6 @@ The following metrics can be controlled by feature flags:
|
|||
|:---------------------------------------------------------------|:-------------------------------------------------------------------|
|
||||
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
|
||||
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
|
||||
| `gitlab_issuable_fast_count_by_state_total` | `soft_fail_count_by_state` |
|
||||
| `gitlab_issuable_fast_count_by_state_failures_total` | `soft_fail_count_by_state` |
|
||||
|
||||
## Sidekiq metrics
|
||||
|
||||
|
|
|
@ -307,8 +307,12 @@ by testing the following commands:
|
|||
```shell
|
||||
sudo mkdir /gitlab-nfs/test-dir
|
||||
sudo chown git /gitlab-nfs/test-dir
|
||||
sudo chgrp gitlab-www /gitlab-nfs/test-dir
|
||||
sudo chgrp root /gitlab-nfs/test-dir
|
||||
sudo chmod 0700 /gitlab-nfs/test-dir
|
||||
sudo chgrp gitlab-www /gitlab-nfs/test-dir
|
||||
sudo chmod 0751 /gitlab-nfs/test-dir
|
||||
sudo chgrp git /gitlab-nfs/test-dir
|
||||
sudo chmod 2770 /gitlab-nfs/test-dir
|
||||
sudo chmod 2755 /gitlab-nfs/test-dir
|
||||
sudo -u git mkdir /gitlab-nfs/test-dir/test2
|
||||
sudo -u git chmod 2755 /gitlab-nfs/test-dir/test2
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Enablement
|
||||
group: Memory
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
|
|
|
@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5 TB of data. Although this
|
||||
|
|
|
@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5 TB of data. Although this
|
||||
|
|
|
@ -356,6 +356,13 @@ are supported and can be added if needed.
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5TB of data. Although this
|
||||
|
|
|
@ -1058,6 +1058,13 @@ Refer to your preferred Load Balancer's documentation for further guidance.
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5 TB of data. Although this
|
||||
|
|
|
@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5 TB of data. Although this
|
||||
|
|
|
@ -1057,6 +1057,13 @@ Refer to your preferred Load Balancer's documentation for further guidance.
|
|||
|
||||
## Configure Gitaly
|
||||
|
||||
NOTE: **Note:**
|
||||
[Gitaly Cluster](../gitaly/praefect.md) support
|
||||
for the Reference Architectures is being
|
||||
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
|
||||
some Architecture specs will likely change as a result to support the new
|
||||
and improved designed.
|
||||
|
||||
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
|
||||
specifically the number of projects and those projects' sizes. It's recommended
|
||||
that a Gitaly server node stores no more than 5 TB of data. Although this
|
||||
|
|
1020
doc/api/projects.md
1020
doc/api/projects.md
File diff suppressed because it is too large
Load Diff
|
@ -38,7 +38,7 @@ addressed.
|
|||
|
||||
## How to create an A/B test
|
||||
|
||||
### Implementation
|
||||
### Implement the experiment
|
||||
|
||||
1. Add the experiment to the `Gitlab::Experimentation::EXPERIMENTS` hash in [`experimentation.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib%2Fgitlab%2Fexperimentation.rb):
|
||||
|
||||
|
@ -50,7 +50,7 @@ addressed.
|
|||
# Add your experiment here:
|
||||
signup_flow: {
|
||||
environment: ::Gitlab.dev_env_or_com?, # Target environment, defaults to enabled for development and GitLab.com
|
||||
tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
|
||||
tracking_category: 'Growth::Activation::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
|
||||
}
|
||||
}.freeze
|
||||
```
|
||||
|
@ -111,8 +111,131 @@ addressed.
|
|||
end
|
||||
```
|
||||
|
||||
1. Track necessary events. See the [product analytics guide](../product_analytics/index.md) for details.
|
||||
1. After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) in the
|
||||
### Implement the tracking events
|
||||
|
||||
To determine whether the experiment is a success or not, we must implement tracking events
|
||||
to acquire data for analyzing. We can send events to Snowplow via either the backend or frontend.
|
||||
Read the [product analytics guide](../product_analytics/index.md) for more details.
|
||||
|
||||
#### Track backend events
|
||||
|
||||
The framework provides the following helper method that is available in controllers:
|
||||
|
||||
```ruby
|
||||
before_action do
|
||||
track_experiment_event(:signup_flow, 'action', 'value')
|
||||
end
|
||||
```
|
||||
|
||||
Which can be tested as follows:
|
||||
|
||||
```ruby
|
||||
context 'when the experiment is active and the user is in the experimental group' do
|
||||
before do
|
||||
stub_experiment(signup_flow: true)
|
||||
stub_experiment_for_user(signup_flow: true)
|
||||
end
|
||||
|
||||
it 'tracks an event', :snowplow do
|
||||
subject
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Growth::Activation::Experiment::SignUpFlow',
|
||||
action: 'action',
|
||||
label: 'value',
|
||||
label: 'experimentation_subject_id',
|
||||
property: 'experimental_group'
|
||||
)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Track frontend events
|
||||
|
||||
The framework provides the following helper method that is available in controllers:
|
||||
|
||||
```ruby
|
||||
before_action do
|
||||
push_frontend_experiment(:signup_flow)
|
||||
frontend_experimentation_tracking_data(:signup_flow, 'action', 'value')
|
||||
end
|
||||
```
|
||||
|
||||
This pushes tracking data to `gon.experiments` and `gon.tracking_data`.
|
||||
|
||||
```ruby
|
||||
expect(Gon.experiments['signupFlow']).to eq(true)
|
||||
|
||||
expect(Gon.tracking_data).to eq(
|
||||
{
|
||||
category: 'Growth::Activation::Experiment::SignUpFlow',
|
||||
action: 'action',
|
||||
value: 'value',
|
||||
label: 'experimentation_subject_id',
|
||||
property: 'experimental_group'
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Which can then be used for tracking as follows:
|
||||
|
||||
```javascript
|
||||
import { isExperimentEnabled } from '~/lib/utils/experimentation';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const signupFlowExperimentEnabled = isExperimentEnabled('signupFlow');
|
||||
|
||||
if (signupFlowExperimentEnabled && gon.tracking_data) {
|
||||
const { category, action, ...data } = gon.tracking_data;
|
||||
|
||||
Tracking.event(category, action, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Which can be tested in Jest as follows:
|
||||
|
||||
```javascript
|
||||
import { withGonExperiment } from 'helpers/experimentation_helper';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
describe('event tracking', () => {
|
||||
describe('with tracking data', () => {
|
||||
withGonExperiment('signupFlow');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
|
||||
|
||||
gon.tracking_data = {
|
||||
category: 'Growth::Activation::Experiment::SignUpFlow',
|
||||
action: 'action',
|
||||
value: 'value',
|
||||
label: 'experimentation_subject_id',
|
||||
property: 'experimental_group'
|
||||
};
|
||||
});
|
||||
|
||||
it('should track data', () => {
|
||||
performAction()
|
||||
|
||||
expect(Tracking.event).toHaveBeenCalledWith(
|
||||
'Growth::Activation::Experiment::SignUpFlow',
|
||||
'action',
|
||||
{
|
||||
value: 'value',
|
||||
label: 'experimentation_subject_id',
|
||||
property: 'experimental_group'
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Enable the experiment
|
||||
|
||||
After all merge requests have been merged, use [`chatops`](../../ci/chatops/README.md) in the
|
||||
[appropriate channel](../feature_flags/controls.md#communicate-the-change) to start the experiment for 10% of the users.
|
||||
The feature flag should have the name of the experiment with the `_experiment_percentage` suffix appended.
|
||||
For visibility, please also share any commands run against production in the `#s_growth` channel:
|
||||
|
@ -127,9 +250,39 @@ For visibility, please also share any commands run against production in the `#s
|
|||
/chatops run feature delete signup_flow_experiment_percentage
|
||||
```
|
||||
|
||||
### Tests and test helpers
|
||||
### Testing and test helpers
|
||||
|
||||
Use the following in Jest to test the experiment is enabled.
|
||||
#### RSpec
|
||||
|
||||
Use the folowing in RSpec to mock the experiment:
|
||||
|
||||
```ruby
|
||||
context 'when the experiment is active' do
|
||||
before do
|
||||
stub_experiment(signup_flow: true)
|
||||
end
|
||||
|
||||
context 'when the user is in the experimental group' do
|
||||
before do
|
||||
stub_experiment_for_user(signup_flow: true)
|
||||
end
|
||||
|
||||
it { is_expected.to do_experimental_thing }
|
||||
end
|
||||
|
||||
context 'when the user is in the control group' do
|
||||
before do
|
||||
stub_experiment_for_user(signup_flow: false)
|
||||
end
|
||||
|
||||
it { is_expected.to do_control_thing }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Jest
|
||||
|
||||
Use the following in Jest to mock the experiment:
|
||||
|
||||
```javascript
|
||||
import { withGonExperiment } from 'helpers/experimentation_helper';
|
||||
|
|
|
@ -340,10 +340,10 @@ These results can also be placed into a PostgreSQL database by setting the
|
|||
`RSPEC_PROFILING_POSTGRES_URL` variable. This is used to profile the test suite
|
||||
when running in the CI environment.
|
||||
|
||||
We store these results also when running CI jobs on the default branch on
|
||||
`gitlab.com`. Statistics of these profiling data are [available
|
||||
online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For example,
|
||||
you can find which tests take longest to run or which execute the most
|
||||
We store these results also when running nightly scheduled CI jobs on the
|
||||
default branch on `gitlab.com`. Statistics of these profiling data are
|
||||
[available online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For
|
||||
example, you can find which tests take longest to run or which execute the most
|
||||
queries. This can be handy for optimizing our tests or identifying performance
|
||||
issues in our code.
|
||||
|
||||
|
|
|
@ -148,6 +148,7 @@ GitLab version | Minimum PostgreSQL version
|
|||
-|-
|
||||
10.0 | 9.6
|
||||
13.0 | 11
|
||||
13.6 | 12
|
||||
|
||||
You must also ensure the `pg_trgm` and `btree_gist` extensions are [loaded into every
|
||||
GitLab database](postgresql_extensions.html).
|
||||
|
|
|
@ -151,6 +151,15 @@ dnf install gitlab-ee-12.0.12-ee.0.el8
|
|||
zypper install gitlab-ee=12.0.12-ee.0
|
||||
```
|
||||
|
||||
To identify the GitLab version number in your package manager, run the following commands:
|
||||
|
||||
```shell
|
||||
# apt-cache (Ubuntu/Debian)
|
||||
sudo apt-cache madison gitlab-ee
|
||||
# yum (RHEL/CentOS 6 and 7)
|
||||
yum --showduplicates list gitlab-ee
|
||||
```
|
||||
|
||||
## Patch releases
|
||||
|
||||
Patch releases **only include bug fixes** for the current stable released version of
|
||||
|
|
|
@ -23,7 +23,10 @@ you can run fuzz tests as part your CI/CD workflow.
|
|||
- SOAP
|
||||
- GraphQL
|
||||
- Form bodies, JSON, or XML
|
||||
- An OpenAPI definition, or HTTP Archive (HAR) of requests to test
|
||||
- One of the following assets to provide APIs to test:
|
||||
- OpenAPI v2 API definition
|
||||
- HTTP Archive (HAR) of API requests to test
|
||||
- Postman Collection v2.0 or v2.1
|
||||
|
||||
## When fuzzing scans run
|
||||
|
||||
|
@ -48,15 +51,17 @@ changes, other pipelines, or other scanners) during a scan could cause inaccurat
|
|||
|
||||
## Configuration
|
||||
|
||||
There are two ways to perform scans. See the configuration section for the one you wish to use:
|
||||
There are three ways to perform scans. See the configuration section for the one you wish to use:
|
||||
|
||||
- [OpenAPI v2 specification](#openapi-specification)
|
||||
- [HTTP Archive (HAR)](#http-archive-har)
|
||||
- [Postman Collection v2.0 or v2.1](#postman-collection)
|
||||
|
||||
Examples of both configurations can be found here:
|
||||
|
||||
- [Example OpenAPI v2 specification project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing-example/-/tree/openapi)
|
||||
- [Example HTTP Archive (HAR) project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing-example/-/tree/har)
|
||||
- [Example Postman Collection project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing/postman-collection/)
|
||||
|
||||
### OpenAPI Specification
|
||||
|
||||
|
@ -229,6 +234,97 @@ DANGER: **Warning:**
|
|||
the API can, it may also trigger bugs in the API. This includes actions like modifying and deleting
|
||||
data. Only run fuzzing against a test server.
|
||||
|
||||
### Postman Collection
|
||||
|
||||
The [Postman API Client](https://www.postman.com/product/api-client/) is a popular tool that
|
||||
developers and testers use to call various types of APIs. The API definitions
|
||||
[can be exported as a Postman Collection file](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/#exporting-postman-data)
|
||||
for use with API Fuzzing. When exporting, make sure to select a supported version of Postman
|
||||
Collection: v2.0 or v2.1.
|
||||
|
||||
When used with GitLab's API fuzzer, Postman Collections must contain definitions of the web API to
|
||||
test with valid data. The API fuzzer extracts all the API definitions and uses them to perform
|
||||
testing.
|
||||
|
||||
DANGER: **Warning:**
|
||||
Postman Collection files may contain sensitive information such as authentication tokens, API keys,
|
||||
and session cookies. We recommend that you review the Postman Collection file contents before adding
|
||||
them to a repository.
|
||||
|
||||
Follow these steps to configure API fuzzing to use a Postman Collection file that provides
|
||||
information about the target API to test:
|
||||
|
||||
1. To use API fuzzing, you must [include](../../../ci/yaml/README.md#includetemplate)
|
||||
the [`API-Fuzzing.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml)
|
||||
that's provided as part of your GitLab installation. To do so, add the following to your
|
||||
`.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: API-Fuzzing.gitlab-ci.yml
|
||||
```
|
||||
|
||||
1. Add the configuration file [`gitlab-api-fuzzing-config.yml`](https://gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing/-/blob/master/gitlab-api-fuzzing-config.yml)
|
||||
to your repository's root as `.gitlab-api-fuzzing.yml`.
|
||||
|
||||
1. The [configuration file](#configuration-files) has several testing profiles defined with varying
|
||||
amounts of fuzzing. We recommend that you start with the `Quick-10` profile. Testing with this
|
||||
profile completes quickly, allowing for easier configuration validation.
|
||||
|
||||
Provide the profile by adding the `FUZZAPI_PROFILE` variable to your `.gitlab-ci.yml` file,
|
||||
substituting `Quick-10` for the profile you choose:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: API-Fuzzing.gitlab-ci.yml
|
||||
|
||||
variables:
|
||||
FUZZAPI_PROFILE: Quick-10
|
||||
```
|
||||
|
||||
1. Add the `FUZZAPI_POSTMAN_COLLECTION` variable and set it to the Postman Collection's location:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: API-Fuzzing.gitlab-ci.yml
|
||||
|
||||
variables:
|
||||
FUZZAPI_PROFILE: Quick-10
|
||||
FUZZAPI_POSTMAN_COLLECTION: postman-collection_serviceA.json
|
||||
```
|
||||
|
||||
1. The target API instance's base URL is also required. Provide it by using the `FUZZAPI_TARGET_URL`
|
||||
variable or an `environment_url.txt` file.
|
||||
|
||||
Adding the URL in an `environment_url.txt` file at your project's root is great for testing in
|
||||
dynamic environments. To run API fuzzing against an app dynamically created during a GitLab CI/CD
|
||||
pipeline, have the app persist its domain in an `environment_url.txt` file. API fuzzing
|
||||
automatically parses that file to find its scan target. You can see an
|
||||
[example of this in our Auto DevOps CI YAML](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml).
|
||||
|
||||
Here's an example of using `FUZZAPI_TARGET_URL`:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: API-Fuzzing.gitlab-ci.yml
|
||||
|
||||
variables:
|
||||
FUZZAPI_PROFILE: Quick-10
|
||||
FUZZAPI_POSTMAN_COLLECTION: postman-collection_serviceA.json
|
||||
FUZZAPI_TARGET_URL: http://test-deployment/
|
||||
```
|
||||
|
||||
This is a minimal configuration for API Fuzzing. From here you can:
|
||||
|
||||
- [Run your first scan](#running-your-first-scan).
|
||||
- [Add authentication](#authentication).
|
||||
- Learn how to [handle false positives](#handling-false-positives).
|
||||
|
||||
DANGER: **Warning:**
|
||||
**NEVER** run fuzz testing against a production server. Not only can it perform *any* function that
|
||||
the API can, it may also trigger bugs in the API. This includes actions like modifying and deleting
|
||||
data. Only run fuzzing against a test server.
|
||||
|
||||
### Authentication
|
||||
|
||||
Authentication is handled by providing the authentication token as a header or cookie. You can
|
||||
|
@ -398,6 +494,7 @@ increases as the numbers go up. To use a configuration file, add it to your repo
|
|||
| `FUZZAPI_REPORT` |Scan report filename. Defaults to `gl-api_fuzzing-report.xml`. |
|
||||
|[`FUZZAPI_OPENAPI`](#openapi-specification)|OpenAPI specification file or URL. |
|
||||
|[`FUZZAPI_HAR`](#http-archive-har)|HTTP Archive (HAR) file. |
|
||||
|[`FUZZAPI_POSTMAN_COLLECTION`](#postman-collection)|Postman Collection file. |
|
||||
|[`FUZZAPI_OVERRIDES_FILE`](#overrides) |Path to a JSON file containing overrides. |
|
||||
|[`FUZZAPI_OVERRIDES_ENV`](#overrides) |JSON string containing headers to override. |
|
||||
|[`FUZZAPI_OVERRIDES_CMD`](#overrides) |Overrides command. |
|
||||
|
|
|
@ -61,11 +61,17 @@ apifuzzer_fuzz:
|
|||
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
|
||||
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
when: never
|
||||
- if: $FUZZAPI_HAR == null && $FUZZAPI_OPENAPI == null
|
||||
when: never
|
||||
- if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
|
||||
script:
|
||||
#
|
||||
# Validate options
|
||||
- |
|
||||
if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI$FUZZAPI_POSTMAN_COLLECTION" == "" ]; then \
|
||||
echo "Error: One of FUZZAPI_HAR, FUZZAPI_OPENAPI, or FUZZAPI_POSTMAN_COLLECTION must be provided."; \
|
||||
echo "See https://docs.gitlab.com/ee/user/application_security/api_fuzzing/ for information on how to configure API Fuzzing."; \
|
||||
exit 1; \
|
||||
fi
|
||||
#
|
||||
# Run user provided pre-script
|
||||
- sh -c "$FUZZAPI_PRE_SCRIPT"
|
||||
#
|
||||
|
@ -96,8 +102,6 @@ apifuzzer_fuzz_dnd:
|
|||
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
|
||||
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
|
||||
when: never
|
||||
- if: $FUZZAPI_HAR == null && $FUZZAPI_OPENAPI == null
|
||||
when: never
|
||||
- if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
|
||||
services:
|
||||
- docker:19.03.12-dind
|
||||
|
@ -142,7 +146,7 @@ apifuzzer_fuzz_dnd:
|
|||
# Start worker container if provided
|
||||
- |
|
||||
if [ "$FUZZAPI_D_WORKER_IMAGE" != "" ]; then \
|
||||
echo "Starting worker image $FUZZAPI_D_WORKER_IMAGE" \
|
||||
echo "Starting worker image $FUZZAPI_D_WORKER_IMAGE"; \
|
||||
docker run \
|
||||
--name worker \
|
||||
--network $FUZZAPI_D_NETWORK \
|
||||
|
@ -153,6 +157,7 @@ apifuzzer_fuzz_dnd:
|
|||
-e FUZZAPI_REPORT \
|
||||
-e FUZZAPI_HAR \
|
||||
-e FUZZAPI_OPENAPI \
|
||||
-e FUZZAPI_POSTMAN_COLLECTION \
|
||||
-e FUZZAPI_TARGET_URL \
|
||||
-e FUZZAPI_OVERRIDES_FILE \
|
||||
-e FUZZAPI_OVERRIDES_ENV \
|
||||
|
@ -174,6 +179,11 @@ apifuzzer_fuzz_dnd:
|
|||
# Start API Fuzzing provided worker if no other worker present
|
||||
- |
|
||||
if [ "$FUZZAPI_D_WORKER_IMAGE" == "" ]; then \
|
||||
if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI$FUZZAPI_POSTMAN_COLLECTION" == "" ]; then \
|
||||
echo "Error: One of FUZZAPI_HAR, FUZZAPI_OPENAPI, or FUZZAPI_POSTMAN_COLLECTION must be provided."; \
|
||||
echo "See https://docs.gitlab.com/ee/user/application_security/api_fuzzing/ for information on how to configure API Fuzzing."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
docker run \
|
||||
--name worker \
|
||||
--network $FUZZAPI_D_NETWORK \
|
||||
|
@ -185,6 +195,7 @@ apifuzzer_fuzz_dnd:
|
|||
-e FUZZAPI_REPORT \
|
||||
-e FUZZAPI_HAR \
|
||||
-e FUZZAPI_OPENAPI \
|
||||
-e FUZZAPI_POSTMAN_COLLECTION \
|
||||
-e FUZZAPI_TARGET_URL \
|
||||
-e FUZZAPI_OVERRIDES_FILE \
|
||||
-e FUZZAPI_OVERRIDES_ENV \
|
||||
|
|
|
@ -1062,16 +1062,6 @@ msgstr ""
|
|||
msgid "0t1DgySidms"
|
||||
msgstr ""
|
||||
|
||||
msgid "1 %{type} addition"
|
||||
msgid_plural "%{count} %{type} additions"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 %{type} modification"
|
||||
msgid_plural "%{count} %{type} modifications"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 Day"
|
||||
msgid_plural "%d Days"
|
||||
msgstr[0] ""
|
||||
|
@ -9109,6 +9099,9 @@ msgstr ""
|
|||
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9825,6 +9818,9 @@ msgstr ""
|
|||
msgid "Email the pipelines status to a list of recipients."
|
||||
msgstr ""
|
||||
|
||||
msgid "Email updates (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies."
|
||||
msgstr ""
|
||||
|
||||
|
@ -13638,7 +13634,7 @@ msgstr ""
|
|||
msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)"
|
||||
msgstr ""
|
||||
|
||||
msgid "I'd like to receive updates via email about GitLab"
|
||||
msgid "I'd like to receive updates about GitLab via email"
|
||||
msgstr ""
|
||||
|
||||
msgid "ID"
|
||||
|
@ -14403,6 +14399,9 @@ msgstr ""
|
|||
msgid "Integrations|Connection successful."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Create new issue in Jira"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Default settings are inherited from the group level."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14418,6 +14417,9 @@ msgstr ""
|
|||
msgid "Integrations|Includes commit title and branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14430,9 +14432,15 @@ msgstr ""
|
|||
msgid "Integrations|Saving will update the default settings for all projects that are not using custom settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Search Jira issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Standard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|To keep this project going, create a new issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Update your projects on Packagist, the main Composer repository"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23310,6 +23318,9 @@ msgstr ""
|
|||
msgid "Search Jira issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search a group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search an environment spec"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ COPY VERSION ./ee/app/models/license.r[b] /home/gitlab/ee/app/models/
|
|||
COPY ./lib/gitlab.rb /home/gitlab/lib/
|
||||
COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/
|
||||
COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/
|
||||
RUN cd /home/gitlab/qa/ && gem install bundler:1.17.3 && bundle install --jobs=$(nproc) --retry=3 --without=development --quiet
|
||||
RUN cd /home/gitlab/qa/ && bundle install --jobs=$(nproc) --retry=3 --without=development --quiet
|
||||
COPY ./qa /home/gitlab/qa
|
||||
|
||||
ENTRYPOINT ["bin/test"]
|
||||
|
|
|
@ -175,4 +175,4 @@ DEPENDENCIES
|
|||
timecop (~> 0.9.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
2.1.4
|
||||
|
|
|
@ -5,8 +5,6 @@ export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
|
|||
export BUNDLE_INSTALL_FLAGS=${BUNDLE_INSTALL_FLAGS:-"--without=production development --jobs=$(nproc) --path=vendor --retry=3 --quiet"}
|
||||
|
||||
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
|
||||
# This is for backwards compatibility for Gitaly
|
||||
run_timed_command "gem install bundler:1.17.3"
|
||||
bundle --version
|
||||
run_timed_command "bundle install --clean ${BUNDLE_INSTALL_FLAGS}"
|
||||
run_timed_command "bundle check"
|
||||
|
|
|
@ -46,6 +46,20 @@ RSpec.describe 'IDE user sees editor info', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'persists position after rename' do
|
||||
ide_open_file('README.md')
|
||||
ide_set_editor_position(4, 10)
|
||||
|
||||
ide_open_file('files/js/application.js')
|
||||
ide_rename_file('README.md', 'READING_RAINBOW.md')
|
||||
|
||||
ide_open_file('READING_RAINBOW.md')
|
||||
|
||||
within find('.ide-status-bar') do
|
||||
expect(page).to have_content('4:10')
|
||||
end
|
||||
end
|
||||
|
||||
it 'persists position' do
|
||||
ide_open_file('README.md')
|
||||
ide_set_editor_position(4, 10)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
|
||||
import { file } from '../../helpers';
|
||||
import { removeWhitespace } from '../../../helpers/text_helper';
|
||||
|
||||
describe('Multi-file editor commit sidebar list collapsed', () => {
|
||||
let vm;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
const Component = Vue.extend(listCollapsed);
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
files: [
|
||||
{
|
||||
...file('file1'),
|
||||
tempFile: true,
|
||||
},
|
||||
file('file2'),
|
||||
],
|
||||
iconName: 'staged',
|
||||
title: 'Staged',
|
||||
});
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders added & modified files count', () => {
|
||||
expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1');
|
||||
});
|
||||
|
||||
describe('addedFilesLength', () => {
|
||||
it('returns an length of temp files', () => {
|
||||
expect(vm.addedFilesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('modifiedFilesLength', () => {
|
||||
it('returns an length of modified files', () => {
|
||||
expect(vm.modifiedFilesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addedFilesIconClass', () => {
|
||||
it('includes multi-file-addition when addedFiles is not empty', () => {
|
||||
expect(vm.addedFilesIconClass).toContain('multi-file-addition');
|
||||
});
|
||||
|
||||
it('excludes multi-file-addition when addedFiles is empty', () => {
|
||||
vm.files = [];
|
||||
|
||||
expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modifiedFilesClass', () => {
|
||||
it('includes multi-file-modified when addedFiles is not empty', () => {
|
||||
expect(vm.modifiedFilesClass).toContain('multi-file-modified');
|
||||
});
|
||||
|
||||
it('excludes multi-file-modified when addedFiles is empty', () => {
|
||||
vm.files = [];
|
||||
|
||||
expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,17 +6,21 @@ import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync
|
|||
|
||||
const TEST_FILE = {
|
||||
name: 'lorem.md',
|
||||
editorRow: 3,
|
||||
editorColumn: 23,
|
||||
fileLanguage: 'markdown',
|
||||
content: 'abc\nndef',
|
||||
permalink: '/lorem.md',
|
||||
};
|
||||
const TEST_FILE_EDITOR = {
|
||||
fileLanguage: 'markdown',
|
||||
editorRow: 3,
|
||||
editorColumn: 23,
|
||||
};
|
||||
const TEST_EDITOR_POSITION = `${TEST_FILE_EDITOR.editorRow}:${TEST_FILE_EDITOR.editorColumn}`;
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('ide/components/ide_status_list', () => {
|
||||
let activeFileEditor;
|
||||
let activeFile;
|
||||
let store;
|
||||
let wrapper;
|
||||
|
@ -27,6 +31,14 @@ describe('ide/components/ide_status_list', () => {
|
|||
getters: {
|
||||
activeFile: () => activeFile,
|
||||
},
|
||||
modules: {
|
||||
editor: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
activeFileEditor: () => activeFileEditor,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMount(IdeStatusList, {
|
||||
|
@ -38,6 +50,7 @@ describe('ide/components/ide_status_list', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
activeFile = TEST_FILE;
|
||||
activeFileEditor = TEST_FILE_EDITOR;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -47,8 +60,6 @@ describe('ide/components/ide_status_list', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
const getEditorPosition = file => `${file.editorRow}:${file.editorColumn}`;
|
||||
|
||||
describe('with regular file', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
@ -65,11 +76,11 @@ describe('ide/components/ide_status_list', () => {
|
|||
});
|
||||
|
||||
it('shows file editor position', () => {
|
||||
expect(wrapper.text()).toContain(getEditorPosition(TEST_FILE));
|
||||
expect(wrapper.text()).toContain(TEST_EDITOR_POSITION);
|
||||
});
|
||||
|
||||
it('shows file language', () => {
|
||||
expect(wrapper.text()).toContain(TEST_FILE.fileLanguage);
|
||||
expect(wrapper.text()).toContain(TEST_FILE_EDITOR.fileLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -81,7 +92,7 @@ describe('ide/components/ide_status_list', () => {
|
|||
});
|
||||
|
||||
it('does not show file editor position', () => {
|
||||
expect(wrapper.text()).not.toContain(getEditorPosition(TEST_FILE));
|
||||
expect(wrapper.text()).not.toContain(TEST_EDITOR_POSITION);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@ describe('RepoEditor', () => {
|
|||
beforeEach(() => {
|
||||
const f = {
|
||||
...file('file.txt'),
|
||||
viewMode: FILE_VIEW_MODE_EDITOR,
|
||||
content: 'hello world',
|
||||
};
|
||||
|
||||
|
@ -92,6 +91,8 @@ describe('RepoEditor', () => {
|
|||
});
|
||||
|
||||
const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder');
|
||||
const changeViewMode = viewMode =>
|
||||
store.dispatch('editor/updateFileEditor', { path: vm.file.path, data: { viewMode } });
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -409,7 +410,7 @@ describe('RepoEditor', () => {
|
|||
describe('when files view mode is preview', () => {
|
||||
beforeEach(done => {
|
||||
jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
|
||||
vm.file.viewMode = FILE_VIEW_MODE_PREVIEW;
|
||||
changeViewMode(FILE_VIEW_MODE_PREVIEW);
|
||||
vm.file.name = 'myfile.md';
|
||||
vm.file.content = 'hello world';
|
||||
|
||||
|
@ -423,7 +424,7 @@ describe('RepoEditor', () => {
|
|||
|
||||
describe('when file view mode changes to editor', () => {
|
||||
it('should update dimensions', () => {
|
||||
vm.file.viewMode = FILE_VIEW_MODE_EDITOR;
|
||||
changeViewMode(FILE_VIEW_MODE_EDITOR);
|
||||
|
||||
return vm.$nextTick().then(() => {
|
||||
expect(vm.editor.updateDimensions).toHaveBeenCalled();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as pathUtils from 'path';
|
||||
import { decorateData } from '~/ide/stores/utils';
|
||||
import { commitActionTypes } from '~/ide/constants';
|
||||
|
||||
export const file = (name = 'name', id = name, type = '', parent = null) =>
|
||||
decorateData({
|
||||
|
@ -28,3 +29,17 @@ export const createEntriesFromPaths = paths =>
|
|||
...entries,
|
||||
};
|
||||
}, {});
|
||||
|
||||
export const createTriggerChangeAction = payload => ({
|
||||
type: 'triggerFilesChange',
|
||||
...(payload ? { payload } : {}),
|
||||
});
|
||||
|
||||
export const createTriggerRenamePayload = (path, newPath) => ({
|
||||
type: commitActionTypes.move,
|
||||
path,
|
||||
newPath,
|
||||
});
|
||||
|
||||
export const createTriggerRenameAction = (path, newPath) =>
|
||||
createTriggerChangeAction(createTriggerRenamePayload(path, newPath));
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as types from '~/ide/stores/mutation_types';
|
|||
import service from '~/ide/services';
|
||||
import { createRouter } from '~/ide/ide_router';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
import { file } from '../../helpers';
|
||||
import { file, createTriggerRenameAction } from '../../helpers';
|
||||
|
||||
const ORIGINAL_CONTENT = 'original content';
|
||||
const RELATIVE_URL_ROOT = '/gitlab';
|
||||
|
@ -785,13 +785,19 @@ describe('IDE store file actions', () => {
|
|||
});
|
||||
|
||||
describe('triggerFilesChange', () => {
|
||||
const { payload: renamePayload } = createTriggerRenameAction('test', '123');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('emits event that files have changed', () => {
|
||||
return store.dispatch('triggerFilesChange').then(() => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change');
|
||||
it.each`
|
||||
args | payload
|
||||
${[]} | ${{}}
|
||||
${[renamePayload]} | ${renamePayload}
|
||||
`('emits event that files have changed (args=$args)', ({ args, payload }) => {
|
||||
return store.dispatch('triggerFilesChange', ...args).then(() => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change', payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '~/ide/stores/actions';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as types from '~/ide/stores/mutation_types';
|
||||
import { file } from '../helpers';
|
||||
import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
|
||||
import testAction from '../../helpers/vuex_action_helper';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
|
||||
|
@ -522,7 +522,7 @@ describe('Multi-file store actions', () => {
|
|||
'path',
|
||||
store.state,
|
||||
[{ type: types.DELETE_ENTRY, payload: 'path' }],
|
||||
[{ type: 'stageChange', payload: 'path' }, { type: 'triggerFilesChange' }],
|
||||
[{ type: 'stageChange', payload: 'path' }, createTriggerChangeAction()],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
@ -551,7 +551,7 @@ describe('Multi-file store actions', () => {
|
|||
[{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }],
|
||||
[
|
||||
{ type: 'stageChange', payload: 'testFolder/entry-to-delete' },
|
||||
{ type: 'triggerFilesChange' },
|
||||
createTriggerChangeAction(),
|
||||
],
|
||||
done,
|
||||
);
|
||||
|
@ -614,7 +614,7 @@ describe('Multi-file store actions', () => {
|
|||
testEntry.path,
|
||||
store.state,
|
||||
[{ type: types.DELETE_ENTRY, payload: testEntry.path }],
|
||||
[{ type: 'stageChange', payload: testEntry.path }, { type: 'triggerFilesChange' }],
|
||||
[{ type: 'stageChange', payload: testEntry.path }, createTriggerChangeAction()],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
@ -754,7 +754,7 @@ describe('Multi-file store actions', () => {
|
|||
payload: origEntry,
|
||||
},
|
||||
],
|
||||
[{ type: 'triggerFilesChange' }],
|
||||
[createTriggerRenameAction('renamed', 'orig')],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
@ -767,7 +767,7 @@ describe('Multi-file store actions', () => {
|
|||
{ path: 'orig', name: 'renamed' },
|
||||
store.state,
|
||||
[expect.objectContaining({ type: types.RENAME_ENTRY })],
|
||||
[{ type: 'triggerFilesChange' }],
|
||||
[createTriggerRenameAction('orig', 'renamed')],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import testAction from 'helpers/vuex_action_helper';
|
||||
import * as types from '~/ide/stores/modules/editor/mutation_types';
|
||||
import * as actions from '~/ide/stores/modules/editor/actions';
|
||||
import { createTriggerRenamePayload } from '../../../helpers';
|
||||
|
||||
describe('~/ide/stores/modules/editor/actions', () => {
|
||||
describe('updateFileEditor', () => {
|
||||
it('commits with payload', () => {
|
||||
const payload = {};
|
||||
|
||||
testAction(actions.updateFileEditor, payload, {}, [
|
||||
{ type: types.UPDATE_FILE_EDITOR, payload },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFileEditor', () => {
|
||||
it('commits with payload', () => {
|
||||
const payload = 'path/to/file.txt';
|
||||
|
||||
testAction(actions.removeFileEditor, payload, {}, [
|
||||
{ type: types.REMOVE_FILE_EDITOR, payload },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameFileEditor', () => {
|
||||
it('commits with payload', () => {
|
||||
const payload = createTriggerRenamePayload('test', 'test123');
|
||||
|
||||
testAction(actions.renameFileEditor, payload, {}, [
|
||||
{ type: types.RENAME_FILE_EDITOR, payload },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
|
||||
import * as getters from '~/ide/stores/modules/editor/getters';
|
||||
|
||||
const TEST_PATH = 'test/path.md';
|
||||
const TEST_FILE_EDITOR = {
|
||||
...createDefaultFileEditor(),
|
||||
editorRow: 7,
|
||||
editorColumn: 8,
|
||||
fileLanguage: 'markdown',
|
||||
};
|
||||
|
||||
describe('~/ide/stores/modules/editor/getters', () => {
|
||||
describe('activeFileEditor', () => {
|
||||
it.each`
|
||||
activeFile | fileEditors | expected
|
||||
${null} | ${{}} | ${null}
|
||||
${{}} | ${{}} | ${createDefaultFileEditor()}
|
||||
${{ path: TEST_PATH }} | ${{}} | ${createDefaultFileEditor()}
|
||||
${{ path: TEST_PATH }} | ${{ bogus: createDefaultFileEditor(), [TEST_PATH]: TEST_FILE_EDITOR }} | ${TEST_FILE_EDITOR}
|
||||
`(
|
||||
'with activeFile=$activeFile and fileEditors=$fileEditors',
|
||||
({ activeFile, fileEditors, expected }) => {
|
||||
const rootGetters = { activeFile };
|
||||
const state = { fileEditors };
|
||||
const result = getters.activeFileEditor(state, {}, {}, rootGetters);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
|
||||
import * as types from '~/ide/stores/modules/editor/mutation_types';
|
||||
import mutations from '~/ide/stores/modules/editor/mutations';
|
||||
import { createTriggerRenamePayload } from '../../../helpers';
|
||||
|
||||
const TEST_PATH = 'test/path.md';
|
||||
|
||||
describe('~/ide/stores/modules/editor/mutations', () => {
|
||||
describe(types.UPDATE_FILE_EDITOR, () => {
|
||||
it('with path that does not exist, should initialize with default values', () => {
|
||||
const state = { fileEditors: {} };
|
||||
const data = { fileLanguage: 'markdown' };
|
||||
|
||||
mutations[types.UPDATE_FILE_EDITOR](state, { path: TEST_PATH, data });
|
||||
|
||||
expect(state.fileEditors).toEqual({
|
||||
[TEST_PATH]: {
|
||||
...createDefaultFileEditor(),
|
||||
...data,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('with existing path, should overwrite values', () => {
|
||||
const state = {
|
||||
fileEditors: {
|
||||
foo: {},
|
||||
[TEST_PATH]: { ...createDefaultFileEditor(), editorRow: 7, editorColumn: 7 },
|
||||
},
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_FILE_EDITOR](state, {
|
||||
path: TEST_PATH,
|
||||
data: { fileLanguage: 'markdown' },
|
||||
});
|
||||
|
||||
expect(state).toEqual({
|
||||
fileEditors: {
|
||||
foo: {},
|
||||
[TEST_PATH]: {
|
||||
...createDefaultFileEditor(),
|
||||
editorRow: 7,
|
||||
editorColumn: 7,
|
||||
fileLanguage: 'markdown',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.REMOVE_FILE_EDITOR, () => {
|
||||
it.each`
|
||||
fileEditors | path | expected
|
||||
${{}} | ${'does/not/exist.txt'} | ${{}}
|
||||
${{ foo: {}, [TEST_PATH]: {} }} | ${TEST_PATH} | ${{ foo: {} }}
|
||||
`('removes file $path', ({ fileEditors, path, expected }) => {
|
||||
const state = { fileEditors };
|
||||
|
||||
mutations[types.REMOVE_FILE_EDITOR](state, path);
|
||||
|
||||
expect(state).toEqual({ fileEditors: expected });
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RENAME_FILE_EDITOR, () => {
|
||||
it.each`
|
||||
fileEditors | payload | expected
|
||||
${{ foo: {} }} | ${createTriggerRenamePayload('does/not/exist', 'abc')} | ${{ foo: {} }}
|
||||
${{ foo: { a: 1 }, bar: {} }} | ${createTriggerRenamePayload('foo', 'abc/def')} | ${{ 'abc/def': { a: 1 }, bar: {} }}
|
||||
`('renames fileEditor at $payload', ({ fileEditors, payload, expected }) => {
|
||||
const state = { fileEditors };
|
||||
|
||||
mutations[types.RENAME_FILE_EDITOR](state, payload);
|
||||
|
||||
expect(state).toEqual({ fileEditors: expected });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import Vuex from 'vuex';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
import { createStoreOptions } from '~/ide/stores';
|
||||
import { setupFileEditorsSync } from '~/ide/stores/modules/editor/setup';
|
||||
import { createTriggerRenamePayload } from '../../../helpers';
|
||||
|
||||
describe('~/ide/stores/modules/editor/setup', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store(createStoreOptions());
|
||||
store.state.entries = {
|
||||
foo: {},
|
||||
bar: {},
|
||||
};
|
||||
store.state.editor.fileEditors = {
|
||||
foo: {},
|
||||
bizz: {},
|
||||
};
|
||||
|
||||
setupFileEditorsSync(store);
|
||||
});
|
||||
|
||||
it('when files change is emitted, removes unused fileEditors', () => {
|
||||
eventHub.$emit('ide.files.change');
|
||||
|
||||
expect(store.state.entries).toEqual({
|
||||
foo: {},
|
||||
bar: {},
|
||||
});
|
||||
expect(store.state.editor.fileEditors).toEqual({
|
||||
foo: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('when files rename is emitted, renames fileEditor', () => {
|
||||
eventHub.$emit('ide.files.change', createTriggerRenamePayload('foo', 'foo_new'));
|
||||
|
||||
expect(store.state.editor.fileEditors).toEqual({
|
||||
foo_new: {},
|
||||
bizz: {},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
import mutations from '~/ide/stores/mutations/file';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
describe('IDE store file mutations', () => {
|
||||
|
@ -532,17 +531,6 @@ describe('IDE store file mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('SET_FILE_VIEWMODE', () => {
|
||||
it('updates file view mode', () => {
|
||||
mutations.SET_FILE_VIEWMODE(localState, {
|
||||
file: localFile,
|
||||
viewMode: FILE_VIEW_MODE_PREVIEW,
|
||||
});
|
||||
|
||||
expect(localFile.viewMode).toBe(FILE_VIEW_MODE_PREVIEW);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADD_PENDING_TAB', () => {
|
||||
beforeEach(() => {
|
||||
const f = { ...file('openFile'), path: 'openFile', active: true, opened: true };
|
||||
|
|
|
@ -261,6 +261,24 @@ describe('IssuableItem', () => {
|
|||
expect(authorEl.text()).toBe(mockAuthor.name);
|
||||
});
|
||||
|
||||
it('renders issuable author info via slot', () => {
|
||||
const wrapperWithAuthorSlot = createComponent({
|
||||
issuableSymbol: '#',
|
||||
issuable: mockIssuable,
|
||||
slots: {
|
||||
reference: `
|
||||
<span class="js-author">${mockAuthor.name}</span>
|
||||
`,
|
||||
},
|
||||
});
|
||||
const authorEl = wrapperWithAuthorSlot.find('.js-author');
|
||||
|
||||
expect(authorEl.exists()).toBe(true);
|
||||
expect(authorEl.text()).toBe(mockAuthor.name);
|
||||
|
||||
wrapperWithAuthorSlot.destroy();
|
||||
});
|
||||
|
||||
it('renders gl-label component for each label present within `issuable` prop', () => {
|
||||
const labelsEl = wrapper.findAll(GlLabel);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
|
||||
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
|
||||
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
|
||||
|
@ -34,6 +34,31 @@ describe('IssuableListRoot', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('skeletonItemCount', () => {
|
||||
it.each`
|
||||
totalItems | defaultPageSize | currentPage | returnValue
|
||||
${100} | ${20} | ${1} | ${20}
|
||||
${105} | ${20} | ${6} | ${5}
|
||||
${7} | ${20} | ${1} | ${7}
|
||||
${0} | ${20} | ${1} | ${5}
|
||||
`(
|
||||
'returns $returnValue when totalItems is $totalItems, defaultPageSize is $defaultPageSize and currentPage is $currentPage',
|
||||
async ({ totalItems, defaultPageSize, currentPage, returnValue }) => {
|
||||
wrapper.setProps({
|
||||
totalItems,
|
||||
defaultPageSize,
|
||||
currentPage,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.skeletonItemCount).toBe(returnValue);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
describe('urlParams', () => {
|
||||
it('updates window URL reflecting props within `urlParams`', async () => {
|
||||
|
@ -111,7 +136,7 @@ describe('IssuableListRoot', () => {
|
|||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(wrapper.vm.skeletonItemCount);
|
||||
});
|
||||
|
||||
it('renders issuable-item component for each item within `issuables` array', () => {
|
||||
|
@ -139,7 +164,7 @@ describe('IssuableListRoot', () => {
|
|||
it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
|
||||
wrapper.setProps({
|
||||
showPaginationControls: true,
|
||||
totalPages: 10,
|
||||
totalItems: 10,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
|
||||
|
||||
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
|
||||
|
||||
import { mergeRequestMeta } from '../mock_data';
|
||||
import { mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
|
||||
|
||||
describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
||||
let wrapper;
|
||||
|
@ -19,6 +19,8 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
|||
propsData: {
|
||||
title,
|
||||
description,
|
||||
templates: mergeRequestTemplates,
|
||||
currentTemplate: null,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
|
@ -31,6 +33,10 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
|||
};
|
||||
|
||||
const findGlFormInputTitle = () => wrapper.find(GlFormInput);
|
||||
const findGlDropdownDescriptionTemplate = () => wrapper.find(GlDropdown);
|
||||
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
|
||||
const findDropdownItemByIndex = index => findAllDropdownItems().at(index);
|
||||
|
||||
const findGlFormTextAreaDescription = () => wrapper.find(GlFormTextarea);
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -49,6 +55,10 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
|||
expect(findGlFormInputTitle().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the description template dropdown', () => {
|
||||
expect(findGlDropdownDescriptionTemplate().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the description input', () => {
|
||||
expect(findGlFormTextAreaDescription().exists()).toBe(true);
|
||||
});
|
||||
|
@ -65,6 +75,11 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
|||
expect(mockGlFormInputTitleInstance.$el.select).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a GlDropdownItem per template plus one (for the starting none option)', () => {
|
||||
expect(findDropdownItemByIndex(0).text()).toBe('None');
|
||||
expect(findAllDropdownItems().length).toBe(mergeRequestTemplates.length + 1);
|
||||
});
|
||||
|
||||
describe('when inputs change', () => {
|
||||
const storageKey = 'sse-merge-request-meta-local-storage-editable';
|
||||
|
||||
|
@ -84,4 +99,17 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
|
|||
expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when templates change', () => {
|
||||
it.each`
|
||||
index | value
|
||||
${0} | ${null}
|
||||
${1} | ${mergeRequestTemplates[0]}
|
||||
${2} | ${mergeRequestTemplates[1]}
|
||||
`('emits a change template event when $index is clicked', ({ index, value }) => {
|
||||
findDropdownItemByIndex(index).vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('changeTemplate')[0][0]).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
|||
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
|
||||
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
|
||||
import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
|
||||
import { sourcePath, mergeRequestMeta } from '../mock_data';
|
||||
import { sourcePath, mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
|
||||
|
||||
describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
|
||||
useLocalStorageSpy();
|
||||
|
@ -15,12 +15,13 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
|
|||
let mockEditMetaControlsInstance;
|
||||
const { title, description } = mergeRequestMeta;
|
||||
|
||||
const buildWrapper = (propsData = {}) => {
|
||||
const buildWrapper = (propsData = {}, data = {}) => {
|
||||
wrapper = shallowMount(EditMetaModal, {
|
||||
propsData: {
|
||||
sourcePath,
|
||||
...propsData,
|
||||
},
|
||||
data: () => data,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -51,7 +52,12 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
|
|||
});
|
||||
|
||||
it('initializes initial merge request meta with local storage data', async () => {
|
||||
const localStorageMeta = { title: 'stored title', description: 'stored description' };
|
||||
const localStorageMeta = {
|
||||
title: 'stored title',
|
||||
description: 'stored description',
|
||||
templates: null,
|
||||
currentTemplate: null,
|
||||
};
|
||||
|
||||
findLocalStorageSync().vm.$emit('input', localStorageMeta);
|
||||
|
||||
|
@ -80,6 +86,14 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
|
|||
expect(findEditMetaControls().props('description')).toBe(description);
|
||||
});
|
||||
|
||||
it('forwards the templates prop', () => {
|
||||
expect(findEditMetaControls().props('templates')).toBe(null);
|
||||
});
|
||||
|
||||
it('forwards the currentTemplate prop', () => {
|
||||
expect(findEditMetaControls().props('currentTemplate')).toBe(null);
|
||||
});
|
||||
|
||||
describe('when save button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
findGlModal().vm.$emit('primary', mergeRequestMeta);
|
||||
|
@ -94,6 +108,36 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when templates exist', () => {
|
||||
const template1 = mergeRequestTemplates[0];
|
||||
|
||||
beforeEach(() => {
|
||||
buildWrapper({}, { templates: mergeRequestTemplates, currentTemplate: null });
|
||||
});
|
||||
|
||||
it('sets the currentTemplate on the changeTemplate event', async () => {
|
||||
findEditMetaControls().vm.$emit('changeTemplate', template1);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findEditMetaControls().props().currentTemplate).toBe(template1);
|
||||
|
||||
findEditMetaControls().vm.$emit('changeTemplate', null);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findEditMetaControls().props().currentTemplate).toBe(null);
|
||||
});
|
||||
|
||||
it('updates the description on the changeTemplate event', async () => {
|
||||
findEditMetaControls().vm.$emit('changeTemplate', template1);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findEditMetaControls().props().description).toEqual(template1.content);
|
||||
});
|
||||
});
|
||||
|
||||
it('emits the hide event', () => {
|
||||
findGlModal().vm.$emit('hide');
|
||||
expect(wrapper.emitted('hide')).toEqual([[]]);
|
||||
|
|
|
@ -48,6 +48,10 @@ export const savedContentMeta = {
|
|||
url: 'foobar/-/merge_requests/123',
|
||||
},
|
||||
};
|
||||
export const mergeRequestTemplates = [
|
||||
{ key: 'Template1', name: 'Template 1', content: 'This is template 1!' },
|
||||
{ key: 'Template2', name: 'Template 2', content: 'This is template 2!' },
|
||||
];
|
||||
|
||||
export const submitChangesError = 'Could not save changes';
|
||||
export const commitBranchResponse = {
|
||||
|
|
|
@ -517,6 +517,8 @@ module TestEnv
|
|||
|
||||
return false if component_matches_git_sha?(component_folder, expected_version)
|
||||
|
||||
return false if component_ahead_of_target?(component_folder, expected_version)
|
||||
|
||||
version = File.read(File.join(component_folder, 'VERSION')).strip
|
||||
|
||||
# Notice that this will always yield true when using branch versions
|
||||
|
@ -527,6 +529,20 @@ module TestEnv
|
|||
true
|
||||
end
|
||||
|
||||
def component_ahead_of_target?(component_folder, expected_version)
|
||||
# The HEAD of the component_folder will be used as heuristic for the version
|
||||
# of the binaries, allowing to use Git to determine if HEAD is later than
|
||||
# the expected version. Note: Git considers HEAD to be an anchestor of HEAD.
|
||||
_out, exit_status = Gitlab::Popen.popen(%W[
|
||||
#{Gitlab.config.git.bin_path}
|
||||
-C #{component_folder}
|
||||
merge-base --is-ancestor
|
||||
#{expected_version} HEAD
|
||||
])
|
||||
|
||||
exit_status == 0
|
||||
end
|
||||
|
||||
def component_matches_git_sha?(component_folder, expected_version)
|
||||
# Not a git SHA, so return early
|
||||
return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID
|
||||
|
|
|
@ -89,4 +89,4 @@ DEPENDENCIES
|
|||
scss_lint (~> 0.56.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
2.1.4
|
||||
|
|
Loading…
Reference in New Issue