Commit dda07ed5 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-04-05' into 'master'

CE upstream - 2018-04-05 12:27 UTC

Closes #4474

See merge request gitlab-org/gitlab-ee!5251
parents 5c918a58 c7351b39
......@@ -891,7 +891,13 @@ qa:selectors:
- bundle exec bin/qa Test::Sanity::Selectors
coverage:
<<: *dedicated-no-docs-no-db-pull-cache-job
# Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
# download artifacts from all the rspec jobs instead of from setup-test-env only
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
variables:
SETUP_DB: "false"
stage: post-test
script:
- bundle exec scripts/merge-simplecov
......
......@@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
......@@ -12,7 +11,6 @@ export default {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
},
......@@ -70,9 +68,6 @@ export default {
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return (
this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
);
},
rawDownloadButtonLabel() {
return this.file.binary ? __('Download') : __('Raw');
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="pull-right ide-btn-group"
>
<a
v-tooltip
:href="file.blamePath"
:title="__('Blame')"
class="btn btn-xs btn-transparent blame"
>
<icon
name="blame"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.commitsPath"
:title="__('History')"
class="btn btn-xs btn-transparent history"
>
<icon
name="history"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.permalink"
:title="__('Permalink')"
class="btn btn-xs btn-transparent permalink"
>
<icon
name="link"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.rawPath"
target="_blank"
class="btn btn-xs btn-transparent prepend-left-10 raw"
rel="noopener noreferrer"
:title="rawDownloadButtonLabel">
<icon
name="download"
:size="16"
/>
</a>
</div>
</template>
......@@ -2,10 +2,16 @@
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
export default {
components: {
ContentViewer,
IdeFileButtons,
},
props: {
file: {
type: Object,
......@@ -18,6 +24,16 @@ export default {
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
editTabCSS() {
return {
active: this.file.viewMode === 'edit',
};
},
previewTabCSS() {
return {
active: this.file.viewMode === 'preview',
};
},
},
watch: {
file(oldVal, newVal) {
......@@ -56,6 +72,7 @@ export default {
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
......@@ -153,15 +170,47 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
v-if="shouldHideEditor"
v-html="file.html"
>
class="ide-mode-tabs clearfix"
v-if="!shouldHideEditor">
<ul class="nav-links pull-left">
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'">
{{ __('Edit') }}
</template>
<template v-else>
{{ __('Review') }}
</template>
</a>
</li>
<li
v-if="file.previewMode"
:class="previewTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode:'preview' })">
{{ file.previewMode.previewTitle }}
</a>
</li>
</ul>
<ide-file-buttons
:file="file"
/>
</div>
<div
v-show="!shouldHideEditor"
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
>
</div>
<content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'"
:content="file.content || file.raw"
:path="file.path"
:project-path="file.projectId"/>
</div>
</template>
<script>
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return this.file.rawPath ||
this.file.blamePath ||
this.file.commitsPath ||
this.file.permalink;
},
rawDownloadButtonLabel() {
return this.file.binary ? 'Download' : 'Raw';
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="multi-file-editor-btn-group"
>
<a
:href="file.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
{{ rawDownloadButtonLabel }}
</a>
<div
class="btn-group"
role="group"
aria-label="File actions"
>
<a
:href="file.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="file.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="file.permalink"
class="btn btn-default btn-sm permalink"
>
Permalink
</a>
</div>
</div>
</template>
......@@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
}
};
export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
......
......@@ -38,6 +38,7 @@ 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 SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
......
......@@ -42,6 +42,7 @@ export default {
renderError: data.render_error,
raw: null,
baseRaw: null,
html: data.html,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......@@ -83,6 +84,11 @@ export default {
mrChange,
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
Object.assign(state.entries[file.path], {
viewMode,
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
......
......@@ -38,6 +38,8 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
viewMode: 'edit',
previewMode: null,
});
export const decorateData = entity => {
......@@ -57,8 +59,9 @@ export const decorateData = entity => {
changed = false,
parentTreeUrl = '',
base64 = false,
previewMode,
file_lock,
html,
} = entity;
return {
......@@ -79,8 +82,9 @@ export const decorateData = entity => {
renderError,
content,
base64,
previewMode,
file_lock,
html,
};
};
......
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => {
const {
data,
projectId,
branchId,
tempFile = false,
content = '',
base64 = false,
} = e.data;
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
......@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${
parentFolder ? `${parentFolder.path}/` : ''
}${folderName}`;
const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
......@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree',
parentTreeUrl: parentFolder
? parentFolder.url
: `/${projectId}/tree/${branchId}/`,
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
......@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
parentTreeUrl: fileFolder
? fileFolder.url
: `/${projectId}/blob/${branchId}`,
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
previewMode: viewerInformationForPath(blobName),
});
Object.assign(acc, {
......
<script>
import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue';
export default {
props: {
content: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: '',
},
},
computed: {
viewer() {
const previewInfo = viewerInformationForPath(this.path);
switch (previewInfo.id) {
case 'markdown':
return MarkdownViewer;
default:
return null;
}
},
},
};
</script>
<template>
<div class="preview-container">
<component
:is="viewer"
:project-path="projectPath"
:content="content"
/>
</div>
</template>
const viewers = {
markdown: {
id: 'markdown',
previewTitle: 'Preview Markdown',
},
};
const fileNameViewers = {};
const fileExtensionViewers = {
md: 'markdown',
markdown: 'markdown',
};
export function viewerInformationForPath(path) {
if (!path) return null;
const name = path.split('/').pop();
const viewerName =
fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || '';
return viewers[viewerName];
}
export default viewers;
<script>
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import $ from 'jquery';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
const CancelToken = axios.CancelToken;
let axiosSource;
export default {
components: {
SkeletonLoadingContainer,
},
props: {
content: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
data() {
return {
previewContent: null,
isLoading: false,
};
},
watch: {
content() {
this.previewContent = null;
},
},
created() {
axiosSource = CancelToken.source();
this.fetchMarkdownPreview();
},
updated() {
this.fetchMarkdownPreview();
},
destroyed() {
if (this.isLoading) axiosSource.cancel('Cancelling Preview');
},
methods: {
fetchMarkdownPreview() {
if (this.content && this.previewContent === null) {
this.isLoading = true;
const postBody = {
text: this.content,
};
const postOptions = {
cancelToken: axiosSource.token,
};
axios
.post(
`${gon.relative_url_root}/${this.projectPath}/preview_markdown`,
postBody,
postOptions,
)
.then(({ data }) => {
this.previewContent = data.body;
this.isLoading = false;
this.$nextTick(() => {
$(this.$refs['markdown-preview']).renderGFM();
});
})
.catch(() => {
this.previewContent = __('An error occurred while fetching markdown preview');
this.isLoading = false;
});
}
},
},
};
</script>
<template>
<div
ref="markdown-preview"
class="md md-previewer">
<skeleton-loading-container v-if="isLoading" />
<div
v-else
v-html="previewContent">
</div>
</div>
</template>
......@@ -308,14 +308,34 @@
height: 100%;
}
.multi-file-editor-btn-group {
padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
.preview-container {
height: 100%;
overflow: auto;
.md-previewer {
padding: $gl-padding;
}
}
.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
background: $white-light;
.nav-links {
border-bottom: 0;
li a {
padding: $gl-padding-8 $gl-padding;
line-height: $gl-btn-line-height;
}
}
}
.ide-btn-group {
padding: $gl-padding-4 $gl-vert-padding;
}
.ide-status-bar {
border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
......
......@@ -166,12 +166,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_save :set_notification_email, if: :email_changed? # in case validation is skipped
before_validation :set_public_email, if: :public_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
......@@ -426,7 +429,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.name = 'Ghost User'
u.notification_email = email
end
end
end
......
......@@ -128,7 +128,7 @@ module ObjectStorage
end
def direct_upload_enabled?
object_store_options.direct_upload
object_store_options&.direct_upload
end
def background_upload_enabled?
......@@ -184,6 +184,14 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
def default_object_store
if self.object_store_enabled? && self.direct_upload_enabled?
Store::REMOTE
else
Store::LOCAL
end
end
end
# allow to configure and overwrite the filename
......@@ -204,12 +212,12 @@ module ObjectStorage
end
def object_store
@object_store ||= model.try(store_serialization_column) || Store::LOCAL
@object_store ||= model.try(store_serialization_column) || self.class.default_object_store
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
@object_store = value || Store::LOCAL
@object_store = value || self.class.default_object_store
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
......
......@@ -152,6 +152,9 @@
%li
excoveralls (Elixir) -
%code \[TOTAL\]\s+(\d+\.\d+)%
%li
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
= f.submit 'Save changes', class: "btn btn-save"
......
---
title: Allow to store uploads by default on Object Storage
merge_request:
author:
type: added
---
title: Ensure internal users (ghost, support bot) get assigned a namespace
merge_request:
author:
type: fixed
......@@ -154,7 +154,7 @@ production: &base
# provider: AWS # Only AWS supported at the moment
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: eu-central-1
# region: us-east-1
## Git LFS
lfs:
......@@ -164,13 +164,14 @@ production: &base
object_store:
enabled: false
remote_directory: lfs-objects # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: eu-central-1
region: us-east-1
# Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
......@@ -184,13 +185,14 @@ production: &base
object_store:
enabled: false
# remote_directory: uploads # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
# connection:
# provider: AWS
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: eu-central-1
connection:
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
......
......@@ -412,6 +412,7 @@ Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system
Settings.uploads['object_store'] ||= Settingslogic.new({})
Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
Settings.uploads['object_store']['direct_upload'] = false if Settings.uploads['object_store']['direct_upload'].nil?
Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
Settings.uploads['object_store']['proxy_download'] = false if Settings.uploads['object_store']['proxy_download'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
......
class ScheduleBuildStageMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
##
# This migration has been rescheduled to run again, see
# `20180405101928_reschedule_builds_stages_migration.rb`
#
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
# noop
end
def down
......
class RescheduleBuildsStagesMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
##
# Rescheduled `20180212101928_schedule_build_stage_migration.rb`
#
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180401213713) do
ActiveRecord::Schema.define(version: 20180405101928) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -65,6 +65,7 @@ For source installations the following settings are nested under `uploads:` and
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Uploads will be stored| |
| `direct_upload` | Set to true to enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. This is beta option as it uses inefficient way of uploading data (via Unicorn). The accelerated uploads gonna be implemented in future releases | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
......
import Vue from 'vue';
import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
import repoFileButtons from '~/ide/components/ide_file_buttons.vue';
import createVueComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
......@@ -23,7 +23,7 @@ describe('RepoFileButtons', () => {
vm.$destroy();
});
it('renders Raw, Blame, History, Permalink and Preview toggle', done => {
it('renders Raw, Blame, History and Permalink', done => {
vm = createComponent();
vm.$nextTick(() => {
......@@ -32,16 +32,30 @@ describe('RepoFileButtons', () => {
const history = vm.$el.querySelector('.history');
expect(raw.href).toMatch(`/${activeFile.rawPath}`);
expect(raw.textContent.trim()).toEqual('Raw');
expect(raw.getAttribute('data-original-title')).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blamePath}`);
expect(blame.textContent.trim()).toEqual('Blame');
expect(blame.getAttribute('data-original-title')).toEqual('Blame');
expect(history.href).toMatch(`/${activeFile.commitsPath}`);
expect(history.textContent.trim()).toEqual('History');
expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual(
expect(history.getAttribute('data-original-title')).toEqual('History');
expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual(
'Permalink',
);
done();
});
});
it('renders Download', done => {
activeFile.binary = true;
vm = createComponent();
vm.$nextTick(() => {
const raw = vm.$el.querySelector('.raw');
expect(raw.href).toMatch(`/${activeFile.rawPath}`);
expect(raw.getAttribute('data-original-title')).toEqual('Download');
done();
});
});
});
......@@ -19,7 +19,6 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
f.html = 'testing';
vm.$store.state.openFiles.push(f);
vm.$store.state.entries[f.path] = f;
vm.monaco = true;
......@@ -47,6 +46,61 @@ describe('RepoEditor', () => {
});
});
it('renders only an edit tab', done => {
Vue.nextTick(() => {
const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
expect(tabs.length).toBe(1);
expect(tabs[0].textContent.trim()).toBe('Edit');
done();
});
});
describe('when file is markdown', () => {
beforeEach(done => {
vm.file.previewMode = {
id: 'markdown',
previewTitle: 'Preview Markdown',
};
vm.$nextTick(done);
});
it('renders an Edit and a Preview Tab', done => {
Vue.nextTick(() => {
const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
expect(tabs.length).toBe(2);
expect(tabs[0].textContent.trim()).toBe('Edit');
expect(tabs[1].textContent.trim()).toBe('Preview Markdown');
done();
});
});
});
describe('when file is markdown and viewer mode is review', () => {
beforeEach(done => {
vm.file.previewMode = {
id: 'markdown',
previewTitle: 'Preview Markdown',
};
vm.$store.state.viewer = 'diff';
vm.$nextTick(done);
});
it('renders an Edit and a Preview Tab', done => {
Vue.nextTick(() => {
const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li');
expect(tabs.length).toBe(2);
expect(tabs[0].textContent.trim()).toBe('Review');
expect(tabs[1].textContent.trim()).toBe('Preview Markdown');
done();
});
});
});
describe('when open file is binary and not raw', () => {
beforeEach(done => {
vm.file.binary = true;
......@@ -57,10 +111,6 @@ describe('RepoEditor', () => {
it('does not render the IDE', () => {
expect(vm.shouldHideEditor).toBeTruthy();
});
it('shows activeFile html', () => {
expect(vm.$el.textContent).toContain('testing');
});
});
describe('createEditorInstance', () => {
......
......@@ -194,6 +194,17 @@ describe('IDE store file mutations', () => {
});
});
describe('SET_FILE_VIEWMODE', () => {
it('updates file view mode', () => {
mutations.SET_FILE_VIEWMODE(localState, {
file: localFile,
viewMode: 'preview',
});
expect(localFile.viewMode).toBe('preview');
});
});
describe('ADD_PENDING_TAB', () => {
beforeEach(() => {
const f = {
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('ContentViewer', () => {
let vm;
let mock;
function createComponent(props) {
const ContentViewer = Vue.extend(contentViewer);
vm = mountComponent(ContentViewer, props);
}
afterEach(() => {
vm.$destroy();
if (mock) mock.restore();
});
it('markdown preview renders + loads rendered markdown from server', done => {
mock = new MockAdapter(axios);
mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, {
body: '<b>testing</b>',
});
createComponent({
path: 'test.md',
content: '* Test',
projectPath: 'testproject',
});
const previewContainer = vm.$el.querySelector('.md-previewer');
setTimeout(() => {
expect(previewContainer.textContent).toContain('testing');
done();
});
});
});
......@@ -7,9 +7,9 @@ describe RenameUsersWithRenamedNamespace, :delete do
other_user1 = create(:user, username: 'api0')
user = create(:user, username: "Users0")
user.update_attribute(:username, 'Users')
user.update_column(:username, 'Users')
user1 = create(:user, username: "import0")
user1.update_attribute(:username, 'import')
user1.update_column(:username, 'import')
described_class.new.up
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
require Rails.root.join('db', 'post_migrate', '20180405101928_reschedule_builds_stages_migration')
describe ScheduleBuildStageMigration, :sidekiq, :migration do
describe RescheduleBuildsStagesMigration, :sidekiq, :migration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
......@@ -10,7 +11,8 @@ describe ScheduleBuildStageMigration, :sidekiq, :migration do
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
namespaces.create(id: 12, name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 123, namespace_id: 12, name: 'gitlab', path: 'gitlab')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test')
......
......@@ -2174,6 +2174,8 @@ describe User do
expect(ghost).to be_ghost
expect(ghost).to be_persisted
expect(ghost.namespace).not_to be_nil
expect(ghost.namespace).to be_persisted
end
it "does not create a second ghost user if one is already present" do
......
......@@ -62,10 +62,12 @@ describe ObjectStorage do
end
describe '#object_store' do
subject { uploader.object_store }
it "delegates to <mount>_store on model" do
expect(object).to receive(:file_store)
uploader.object_store
subject
end
context 'when store is null' do
......@@ -73,8 +75,36 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(nil)
end
it "returns Store::LOCAL" do
expect(uploader.object_store).to eq(described_class::Store::LOCAL)
context 'when object storage is enabled' do
context 'when direct uploads are enabled' do
before do
stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
end
it "uses Store::REMOTE" do
is_expected.to eq(described_class::Store::REMOTE)
end
end
context 'when direct uploads are disabled' do
before do
stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
end
it "uses Store::LOCAL" do
is_expected.to eq(described_class::Store::LOCAL)
end
end
end
context 'when object storage is disabled' do
before do
stub_uploads_object_storage(uploader_class, enabled: false)
end
it "uses Store::LOCAL" do
is_expected.to eq(described_class::Store::LOCAL)
end
end
end
......@@ -84,7 +114,7 @@ describe ObjectStorage do
end
it "returns the given value" do
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
is_expected.to eq(described_class::Store::REMOTE)
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment