Commit f4c80186 authored by James Edwards-Jones's avatar James Edwards-Jones

Merge branch 'master' into jej/branch-unprotection-disable-ui

parents eb6f7a91 cf19910d
...@@ -140,7 +140,7 @@ stages: ...@@ -140,7 +140,7 @@ stages:
# Jobs that only need to pull cache # Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job .dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa <<: *except-docs
<<: *pull-cache <<: *pull-cache
dependencies: dependencies:
- setup-test-env - setup-test-env
...@@ -152,6 +152,10 @@ stages: ...@@ -152,6 +152,10 @@ stages:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
.dedicated-no-docs-and-no-qa-pull-cache-job: &dedicated-no-docs-and-no-qa-pull-cache-job
<<: *dedicated-no-docs-pull-cache-job
<<: *except-docs-and-qa
.rake-exec: &rake-exec .rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
script: script:
...@@ -310,7 +314,7 @@ stages: ...@@ -310,7 +314,7 @@ stages:
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup .gitlab-setup: &gitlab-setup
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg <<: *use-pg
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
...@@ -351,12 +355,12 @@ stages: ...@@ -351,12 +355,12 @@ stages:
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables: variables:
CREATE_DB_USER: "true" CREATE_DB_USER: "true"
script: script:
...@@ -768,7 +772,7 @@ migration:path-mysql: ...@@ -768,7 +772,7 @@ migration:path-mysql:
<<: *use-mysql <<: *use-mysql
.db-rollback: &db-rollback .db-rollback: &db-rollback
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script: script:
- bundle exec rake db:migrate VERSION=20170523121229 - bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate - bundle exec rake db:migrate
...@@ -781,10 +785,9 @@ db:rollback-mysql: ...@@ -781,10 +785,9 @@ db:rollback-mysql:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
db:rollback-pg-geo: &db-rollback db:rollback-pg-geo:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
script: script:
- bundle exec rake geo:db:migrate VERSION=20170627195211 - bundle exec rake geo:db:migrate VERSION=20170627195211
- bundle exec rake geo:db:migrate - bundle exec rake geo:db:migrate
...@@ -799,7 +802,7 @@ gitlab:setup-mysql: ...@@ -799,7 +802,7 @@ gitlab:setup-mysql:
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
dependencies: [] dependencies: []
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
...@@ -820,7 +823,7 @@ gitlab:assets:compile: ...@@ -820,7 +823,7 @@ gitlab:assets:compile:
- webpack-report/ - webpack-report/
karma: karma:
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg <<: *use-pg
dependencies: dependencies:
- compile-assets - compile-assets
...@@ -944,7 +947,7 @@ coverage: ...@@ -944,7 +947,7 @@ coverage:
- coverage/assets/ - coverage/assets/
lint:javascript:report: lint:javascript:report:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test stage: post-test
dependencies: dependencies:
- compile-assets - compile-assets
......
...@@ -25,12 +25,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -25,12 +25,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels) - [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc) - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc) - [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging) - [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
...@@ -128,7 +128,7 @@ Most issues will have labels for at least one of the following: ...@@ -128,7 +128,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc. - Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4 - Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4 - Severity: ~S1, ~S2, ~S3, ~S4
...@@ -172,13 +172,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api, ...@@ -172,13 +172,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
Subject labels are always all-lowercase. Subject labels are always all-lowercase.
### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.) ### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
Team labels specify what team is responsible for this issue. Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate Assigning a team label makes sure issues get the attention of the appropriate
people. people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge, The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX". ~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the The descriptions on the [labels page][labels-page] explain what falls under the
...@@ -746,4 +746,3 @@ When your code contains more than 500 changes, any major breaking changes, or an ...@@ -746,4 +746,3 @@ When your code contains more than 500 changes, any major breaking changes, or an
[^1]: Please note that specs other than JavaScript specs are considered backend [^1]: Please note that specs other than JavaScript specs are considered backend
code. code.
\ No newline at end of file
...@@ -295,7 +295,6 @@ gem 'batch-loader', '~> 1.2.1' ...@@ -295,7 +295,6 @@ gem 'batch-loader', '~> 1.2.1'
gem 'peek', '~> 1.0.1' gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2' gem 'peek-gc', '~> 0.0.2'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0' gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0' gem 'peek-redis', '~> 1.2.0'
......
...@@ -632,8 +632,6 @@ GEM ...@@ -632,8 +632,6 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
peek peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0) peek-pg (1.3.0)
concurrent-ruby concurrent-ruby
concurrent-ruby-ext concurrent-ruby-ext
...@@ -1167,7 +1165,6 @@ DEPENDENCIES ...@@ -1167,7 +1165,6 @@ DEPENDENCIES
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0) peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
......
...@@ -26,11 +26,18 @@ export default { ...@@ -26,11 +26,18 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
forceModifiedIcon: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
changedIcon() { changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`; return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}`
: `file-modified${suffix}`;
}, },
stagedIcon() { stagedIcon() {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
export default { export default {
components: { components: {
icon, icon,
newModal, newModal,
...@@ -27,10 +27,15 @@ ...@@ -27,10 +27,15 @@
dropdownOpen: false, dropdownOpen: false,
}; };
}, },
watch: {
dropdownOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView();
});
},
},
methods: { methods: {
...mapActions([ ...mapActions(['createTempEntry']),
'createTempEntry',
]),
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
this.openModal = true; this.openModal = true;
...@@ -43,7 +48,7 @@ ...@@ -43,7 +48,7 @@
this.dropdownOpen = !this.dropdownOpen; this.dropdownOpen = !this.dropdownOpen;
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -71,7 +76,10 @@ ...@@ -71,7 +76,10 @@
css-classes="pull-left" css-classes="pull-left"
/> />
</button> </button>
<ul class="dropdown-menu dropdown-menu-right"> <ul
class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
>
<li> <li>
<a <a
href="#" href="#"
......
...@@ -40,13 +40,6 @@ export default { ...@@ -40,13 +40,6 @@ export default {
return __('Create file'); return __('Create file');
}, },
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
}, },
mounted() { mounted() {
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
...@@ -82,8 +75,8 @@ export default { ...@@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore" @submit.prevent="createEntryInStore"
> >
<fieldset class="form-group append-bottom-0"> <fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3"> <label class="label-light col-sm-3 ide-new-modal-label">
{{ formLabelName }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-9">
<input <input
......
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
:file="file" :file="file"
/> />
</span> </span>
<span class="pull-right"> <span class="pull-right ide-file-icon-holder">
<mr-file-icon <mr-file-icon
v-if="file.mrChange" v-if="file.mrChange"
/> />
...@@ -106,7 +106,8 @@ export default { ...@@ -106,7 +106,8 @@ export default {
:file="file" :file="file"
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
class="prepend-top-5 pull-right" :force-modified-icon="true"
class="pull-right"
/> />
</span> </span>
<new-dropdown <new-dropdown
......
...@@ -84,6 +84,7 @@ export default { ...@@ -84,6 +84,7 @@ export default {
<changed-file-icon <changed-file-icon
v-else v-else
:file="tab" :file="tab"
:force-modified-icon="true"
/> />
</button> </button>
......
...@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { ...@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
} }
}; };
export const toggleRightPanelCollapsed = ( export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
{ dispatch, state },
e = undefined,
) => {
if (e) { if (e) {
$(e.currentTarget) $(e.currentTarget)
.tooltip('hide') .tooltip('hide')
...@@ -77,7 +74,7 @@ export const createTempEntry = ( ...@@ -77,7 +74,7 @@ export const createTempEntry = (
} }
worker.addEventListener('message', ({ data }) => { worker.addEventListener('message', ({ data }) => {
const { file } = data; const { file, parentPath } = data;
worker.terminate(); worker.terminate();
...@@ -93,6 +90,10 @@ export const createTempEntry = ( ...@@ -93,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path); dispatch('setFileActive', file.path);
} }
if (parentPath && !state.entries[parentPath].opened) {
commit(types.TOGGLE_TREE_OPEN, parentPath);
}
resolve(file); resolve(file);
}); });
...@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
if (file.parentPath) {
dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
}
};
export const toggleFileFinder = ({ commit }, fileFindVisible) => export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible); commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
......
...@@ -63,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -63,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path]; const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
return service return service
.getFileData(file.url) .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
.then(res => { .then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle); setPageTitle(pageTitle);
......
...@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = ( ...@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = (
{ root: true }, { root: true },
); );
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
file,
changed: false,
},
{ root: true },
);
dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
eventHub.$emit(`editor.update.model.content.${file.key}`, { eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content, content: file.content,
changed: !!changedFile, changed: !!changedFile,
......
...@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; ...@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
...@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request'; ...@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
import { sortTree } from './utils';
export default { export default {
[types.SET_INITIAL_DATA](state, data) { [types.SET_INITIAL_DATA](state, data) {
...@@ -73,7 +74,7 @@ export default { ...@@ -73,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined, f => foundEntry.tree.find(e => e.path === f.path) === undefined,
); );
Object.assign(foundEntry, { Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree), tree: sortTree(foundEntry.tree.concat(tree)),
}); });
} }
...@@ -86,10 +87,16 @@ export default { ...@@ -86,10 +87,16 @@ export default {
if (!foundEntry) { if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], { Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList), tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
}); });
} }
}, },
[types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
Object.assign(state.entries[path], {
tempFile,
changed: tempFile,
});
},
[types.UPDATE_VIEWER](state, viewer) { [types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, { Object.assign(state, {
viewer, viewer,
......
...@@ -33,6 +33,7 @@ export const dataStructure = () => ({ ...@@ -33,6 +33,7 @@ export const dataStructure = () => ({
raw: '', raw: '',
content: '', content: '',
parentTreeUrl: '', parentTreeUrl: '',
parentPath: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1, editorRow: 1,
...@@ -65,6 +66,7 @@ export const decorateData = entity => { ...@@ -65,6 +66,7 @@ export const decorateData = entity => {
previewMode, previewMode,
file_lock, file_lock,
html, html,
parentPath = '',
} = entity; } = entity;
return { return {
...@@ -81,6 +83,7 @@ export const decorateData = entity => { ...@@ -81,6 +83,7 @@ export const decorateData = entity => {
opened, opened,
active, active,
parentTreeUrl, parentTreeUrl,
parentPath,
changed, changed,
renderError, renderError,
content, content,
...@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => { ...@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') { } else if (a.type === 'blob' && b.type === 'tree') {
return 1; return 1;
} }
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; if (a.name < b.name) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; if (a.name > b.name) return 1;
return 0; return 0;
}; };
......
...@@ -6,6 +6,7 @@ self.addEventListener('message', e => { ...@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = []; const treeList = [];
let file; let file;
let parentPath;
const entries = data.reduce((acc, path) => { const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/'); const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim(); const blobName = pathSplit.pop().trim();
...@@ -17,6 +18,8 @@ self.addEventListener('message', e => { ...@@ -17,6 +18,8 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath]; const foundEntry = acc[folderPath];
if (!foundEntry) { if (!foundEntry) {
parentPath = parentFolder ? parentFolder.path : null;
const tree = decorateData({ const tree = decorateData({
projectId, projectId,
branchId, branchId,
...@@ -29,6 +32,7 @@ self.addEventListener('message', e => { ...@@ -29,6 +32,7 @@ self.addEventListener('message', e => {
tempFile, tempFile,
changed: tempFile, changed: tempFile,
opened: tempFile, opened: tempFile,
parentPath,
}); });
Object.assign(acc, { Object.assign(acc, {
...@@ -52,6 +56,8 @@ self.addEventListener('message', e => { ...@@ -52,6 +56,8 @@ self.addEventListener('message', e => {
if (blobName !== '') { if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')]; const fileFolder = acc[pathSplit.join('/')];
parentPath = fileFolder ? fileFolder.path : null;
file = decorateData({ file = decorateData({
projectId, projectId,
branchId, branchId,
...@@ -66,6 +72,7 @@ self.addEventListener('message', e => { ...@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content, content,
base64, base64,
previewMode: viewerInformationForPath(blobName), previewMode: viewerInformationForPath(blobName),
parentPath,
}); });
Object.assign(acc, { Object.assign(acc, {
...@@ -86,5 +93,6 @@ self.addEventListener('message', e => { ...@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries, entries,
treeList: sortTree(treeList), treeList: sortTree(treeList),
file, file,
parentPath,
}); });
}); });
...@@ -12,7 +12,8 @@ import ModalStore from './boards/stores/modal_store'; ...@@ -12,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect { export default class MilestoneSelect {
constructor(currentProject, els, options = {}) { constructor(currentProject, els, options = {}) {
if (currentProject !== null) { if (currentProject !== null) {
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; this.currentProject =
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
} }
this.init(els, options); this.init(els, options);
...@@ -26,7 +27,10 @@ export default class MilestoneSelect { ...@@ -26,7 +27,10 @@ export default class MilestoneSelect {
} }
$els.each((i, dropdown) => { $els.each((i, dropdown) => {
let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; let milestoneLinkNoneTemplate,
milestoneLinkTemplate,
selectedMilestone,
selectedMilestoneDefault;
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId'); const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones'); const milestonesUrl = $dropdown.data('milestones');
...@@ -46,45 +50,47 @@ export default class MilestoneSelect { ...@@ -46,45 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value'); const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut(); const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null); selectedMilestoneDefault = showAny ? '' : null;
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault); selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template(
'<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: (term, callback) => axios.get(milestonesUrl) data: (term, callback) =>
.then(({ data }) => { axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = []; const extraOptions = [];
if (showAny) { if (showAny) {
extraOptions.push({ extraOptions.push({
id: 0, id: null,
name: '', name: null,
title: 'Any Milestone' title: 'Any Milestone',
}); });
} }
if (showNo) { if (showNo) {
extraOptions.push({ extraOptions.push({
id: -1, id: -1,
name: 'No Milestone', name: 'No Milestone',
title: 'No Milestone' title: 'No Milestone',
}); });
} }
if (showUpcoming) { if (showUpcoming) {
extraOptions.push({ extraOptions.push({
id: -2, id: -2,
name: '#upcoming', name: '#upcoming',
title: 'Upcoming' title: 'Upcoming',
}); });
} }
if (showStarted) { if (showStarted) {
extraOptions.push({ extraOptions.push({
id: -3, id: -3,
name: '#started', name: '#started',
title: 'Started' title: 'Started',
}); });
} }
if (extraOptions.length) { if (extraOptions.length) {
...@@ -106,7 +112,7 @@ export default class MilestoneSelect { ...@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`, `,
filterable: true, filterable: true,
search: { search: {
fields: ['title'] fields: ['title'],
}, },
selectable: true, selectable: true,
toggleLabel: (selected, el, e) => { toggleLabel: (selected, el, e) => {
...@@ -119,7 +125,7 @@ export default class MilestoneSelect { ...@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel, defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'), fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title), text: milestone => _.escape(milestone.title),
id: (milestone) => { id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name; return milestone.name;
} else { } else {
...@@ -131,7 +137,7 @@ export default class MilestoneSelect { ...@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule // display:block overrides the hide-collapse rule
return $value.css('display', ''); return $value.css('display', '');
}, },
opened: (e) => { opened: e => {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
...@@ -140,7 +146,7 @@ export default class MilestoneSelect { ...@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active'); $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => { clicked: clickEvent => {
const { $el, e } = clickEvent; const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj; let selected = clickEvent.selectedObj;
...@@ -155,11 +161,14 @@ export default class MilestoneSelect { ...@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page'); const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index'; const isIssueIndex = page === 'projects:issues:index';
const isMRIndex = (page === page && page === 'projects:merge_requests:index'); const isMRIndex = page === page && page === 'projects:merge_requests:index';
const isSelecting = (selected.name !== selectedMilestone); const isSelecting = selected.name !== selectedMilestone;
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if (
$dropdown.hasClass('js-filter-bulk-update') ||
$dropdown.hasClass('js-issuable-form-dropdown')
) {
e.preventDefault(); e.preventDefault();
return; return;
} }
...@@ -177,10 +186,13 @@ export default class MilestoneSelect { ...@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) { if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({ gl.issueBoards.boardStoreIssueSet(
'milestone',
new ListMilestone({
id: selected.id, id: selected.id,
title: selected.name title: selected.name,
})); }),
);
} else { } else {
gl.issueBoards.boardStoreIssueDelete('milestone'); gl.issueBoards.boardStoreIssueDelete('milestone');
} }
...@@ -188,7 +200,8 @@ export default class MilestoneSelect { ...@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue
.update($dropdown.attr('data-issue-update'))
.then(() => { .then(() => {
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
...@@ -203,7 +216,8 @@ export default class MilestoneSelect { ...@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null; data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return axios.put(issueUpdateURL, data) return axios
.put(issueUpdateURL, data)
.then(({ data }) => { .then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
...@@ -215,7 +229,10 @@ export default class MilestoneSelect { ...@@ -215,7 +229,10 @@ export default class MilestoneSelect {
data.milestone.name = data.milestone.title; data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue return $sidebarCollapsedValue
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`) .attr(
'data-original-title',
`${data.milestone.name}<br />${data.milestone.remaining}`,
)
.find('span') .find('span')
.text(data.milestone.title); .text(data.milestone.title);
} else { } else {
...@@ -230,7 +247,7 @@ export default class MilestoneSelect { ...@@ -230,7 +247,7 @@ export default class MilestoneSelect {
$loading.fadeOut(); $loading.fadeOut();
}); });
} }
} },
}); });
}); });
} }
......
...@@ -52,16 +52,15 @@ ...@@ -52,16 +52,15 @@
text() { text() {
const keepContributionsText = s__(`AdminArea| const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}. You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them. Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea| const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}. You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{ {
username: `<strong>${_.escape(this.username)}</strong>`, username: `<strong>${_.escape(this.username)}</strong>`,
......
...@@ -188,11 +188,11 @@ export default class ActivityCalendar { ...@@ -188,11 +188,11 @@ export default class ActivityCalendar {
}, },
{ {
text: 'W', text: 'W',
y: 29 + this.dayYPos(2), y: 29 + this.dayYPos(3),
}, },
{ {
text: 'F', text: 'F',
y: 29 + this.dayYPos(3), y: 29 + this.dayYPos(5),
}, },
]; ];
this.svg this.svg
......
...@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service'; ...@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue'; import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue'; import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue'; import simpleMetric from './simple_metric.vue';
import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash'; import Flash from '../../flash';
...@@ -14,7 +13,6 @@ export default { ...@@ -14,7 +13,6 @@ export default {
detailedMetric, detailedMetric,
requestSelector, requestSelector,
simpleMetric, simpleMetric,
upstreamPerformanceBar,
}, },
props: { props: {
store: { store: {
...@@ -128,9 +126,6 @@ export default { ...@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }} {{ currentRequest.details.host.hostname }}
</span> </span>
</div> </div>
<upstream-performance-bar
v-if="initialRequest && currentRequest.details"
/>
<detailed-metric <detailed-metric
v-for="metric in $options.detailedMetrics" v-for="metric in $options.detailedMetrics"
:key="metric.metric" :key="metric.metric"
......
<script>
export default {
mounted() {
const upstreamPerformanceBar = document
.getElementById('peek-view-performance-bar')
.cloneNode(true);
upstreamPerformanceBar.classList.remove('hidden');
this.$refs.wrapper.appendChild(upstreamPerformanceBar);
},
};
</script>
<template>
<div
id="peek-view-performance-bar-vue"
class="view"
ref="wrapper"
></div>
</template>
import 'vendor/peek.performance_bar';
import Vue from 'vue'; import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue'; import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store'; import PerformanceBarStore from './stores/performance_bar_store';
......
<script> <script>
import getIconForFile from './file_icon/file_icon_map'; import getIconForFile from './file_icon/file_icon_map';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite /* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
/> />
*/ */
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
icon, icon,
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
}, },
}; };
</script> </script>
<template> <template>
<span> <span>
...@@ -82,6 +82,7 @@ ...@@ -82,6 +82,7 @@
v-if="!loading && folder" v-if="!loading && folder"
:name="folderIconName" :name="folderIconName"
:size="size" :size="size"
css-classes="folder-icon"
/> />
<loading-icon <loading-icon
v-if="loading" v-if="loading"
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
.fork-svg { .fork-svg {
margin-right: 4px; margin-right: 4px;
vertical-align: bottom;
} }
} }
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
} }
.branch-info .commit-icon { .branch-info .commit-icon {
margin-right: 3px; margin-right: 8px;
svg { svg {
top: 3px; top: 3px;
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: inherit; max-width: inherit;
line-height: 22px;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -67,6 +68,11 @@ ...@@ -67,6 +68,11 @@
} }
} }
.ide-file-icon-holder {
display: flex;
align-items: center;
}
.ide-file-changed-icon { .ide-file-changed-icon {
margin-left: auto; margin-left: auto;
...@@ -77,7 +83,6 @@ ...@@ -77,7 +83,6 @@
.ide-new-btn { .ide-new-btn {
display: none; display: none;
margin-bottom: -4px;
margin-right: -8px; margin-right: -8px;
} }
...@@ -90,12 +95,10 @@ ...@@ -90,12 +95,10 @@
} }
} }
&.folder { .folder-icon {
svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
} }
}
a { a {
color: $gl-text-color; color: $gl-text-color;
...@@ -111,6 +114,7 @@ ...@@ -111,6 +114,7 @@
.file-col-commit-message { .file-col-commit-message {
display: flex; display: flex;
overflow: visible; overflow: visible;
align-items: center;
padding: 6px 12px; padding: 6px 12px;
} }
...@@ -438,7 +442,7 @@ ...@@ -438,7 +442,7 @@
.projects-sidebar { .projects-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; flex: 1;
.context-header { .context-header {
width: auto; width: auto;
...@@ -967,3 +971,7 @@ ...@@ -967,3 +971,7 @@
background: transparent; background: transparent;
resize: none; resize: none;
} }
.ide-new-modal-label {
line-height: 34px;
}
@import 'framework/variables'; @import 'framework/variables';
@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof'; @import 'peek/views/rblineprof';
#js-peek { #js-peek {
......
...@@ -17,7 +17,7 @@ module BlobHelper ...@@ -17,7 +17,7 @@ module BlobHelper
end end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" "#{ide_path}/project#{url_for([project, "edit", "blob", id: [ref, path], script_name: "/"])}"
end end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
......
...@@ -63,7 +63,7 @@ module CommitsHelper ...@@ -63,7 +63,7 @@ module CommitsHelper
# Returns a link formatted as a commit branch link # Returns a link formatted as a commit branch link
def commit_branch_link(url, text) def commit_branch_link(url, text)
link_to(url, class: 'label label-gray ref-name branch-link') do link_to(url, class: 'label label-gray ref-name branch-link') do
sprite_icon('fork', size: 16, css_class: 'fork-svg') + "#{text}" sprite_icon('fork', size: 12, css_class: 'fork-svg') + "#{text}"
end end
end end
......
...@@ -159,7 +159,7 @@ module SystemNoteService ...@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0 body = if noteable.time_estimate == 0
"removed time estimate" "removed time estimate"
else else
"changed time estimate to #{parsed_time}," "changed time estimate to #{parsed_time}"
end end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
delete_user_url: admin_user_path(user), delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: user.name, username: user.name,
delete_contributions: 'false' }, type: 'button' } delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
%li %li
...@@ -58,5 +58,5 @@ ...@@ -58,5 +58,5 @@
delete_user_url: admin_user_path(user, hard_delete: true), delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: user.name, username: user.name,
delete_contributions: 'true' }, type: 'button' } delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions') = s_('AdminUsers|Delete user and contributions')
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
delete_user_url: admin_user_path(@user), delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: @user.name, username: @user.name,
delete_contributions: 'false' }, type: 'button' } delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
...@@ -226,7 +226,7 @@ ...@@ -226,7 +226,7 @@
delete_user_url: admin_user_path(@user, hard_delete: true), delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: @user.name, username: @user.name,
delete_contributions: 'true' }, type: 'button' } delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions') = s_('AdminUsers|Delete user and contributions')
- else - else
%p %p
......
...@@ -5,8 +5,3 @@ ...@@ -5,8 +5,3 @@
peek_url: peek_routes.results_url, peek_url: peek_routes.results_url,
profile_url: url_for(params.merge(lineprofiler: 'true')) }, profile_url: url_for(params.merge(lineprofiler: 'true')) },
class: Peek.env } class: Peek.env }
#peek-view-performance-bar.hidden
= render_server_response_time
%span#serverstats
%ul.performance-bar
- content_for :merge_access_levels do - content_for :merge_access_levels do
.merge_access_levels-container .merge_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide', options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_class: 'dropdown-menu-selectable qa-allowed-to-merge-dropdown capitalize-header',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
- content_for :push_access_levels do - content_for :push_access_levels do
.push_access_levels-container .push_access_levels-container
......
%td %td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
= dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') , = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header', options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }}) data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
%td %td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
......
---
title: Correct text and functionality for delete user / delete user and contributions
modal.
merge_request: 18463
author: Marc Schwede
type: fixed
---
title: Improve performance of a service responsible for creating a pipeline
merge_request: 18582
author:
type: performance
---
title: Fix size and position for fork icon
merge_request: 18449
author: George Tsiolis
type: changed
---
title: Reset milestone filter when clicking "Any Milestone" in dashboard
merge_request: 18531
author:
type: fixed
Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) } Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) }
Peek.into Peek::Views::Host Peek.into Peek::Views::Host
Peek.into Peek::Views::PerformanceBar
if Gitlab::Database.mysql? if Gitlab::Database.mysql?
require 'peek-mysql2' require 'peek-mysql2'
......
...@@ -201,7 +201,7 @@ The following guide assumes that: ...@@ -201,7 +201,7 @@ The following guide assumes that:
for more information. for more information.
1. Save the file and reconfigure GitLab for the database listen changes and 1. Save the file and reconfigure GitLab for the database listen changes and
the replication slot changes to be applied. the replication slot changes to be applied:
```bash ```bash
gitlab-ctl reconfigure gitlab-ctl reconfigure
...@@ -555,6 +555,55 @@ the instructions below: ...@@ -555,6 +555,55 @@ the instructions below:
gitlab-ctl restart gitlab-ctl restart
``` ```
## PGBouncer support (optional)
[PGBouncer](http://pgbouncer.github.io/) may be used with GitLab Geo to pool
PostgreSQL connections. We recommend using PGBouncer if you use GitLab in a
high-availability configuration with a cluster of nodes supporting a Geo
primary and another cluster of nodes supporting a Geo secondary. For more
information, see the [Omnibus HA](https://docs.gitlab.com/ee/administration/high_availability/database.html#configure-using-omnibus-for-high-availability)
documentation.
For a Geo secondary to work properly with PGBouncer in front of the database,
it will need a separate read-only user to make [PostgreSQL FDW queries][FDW]
work:
1. On the primary Geo database, enter the PostgreSQL on the console as an
admin user. If you are using an Omnibus-managed database, log onto the primary
node that is running the PostgreSQL database:
```bash
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql -h /var/opt/gitlab/postgresql gitlabhq_production
```
2. Then create the read-only user:
```sql
-- NOTE: Use the password defined earlier
CREATE USER gitlab_geo_fdw WITH password 'mypassword';
GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
-- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
-- automatically.
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
```
3. On the Geo secondary nodes, change `/etc/gitlab/gitlab.rb`:
```
geo_postgresql['fdw_external_user'] = 'gitlab_geo_fdw'
```
4. Save the file and reconfigure GitLab for the changes to be applied:
```bash
gitlab-ctl reconfigure
```
## MySQL replication ## MySQL replication
MySQL replication is not supported for Geo. MySQL replication is not supported for Geo.
......
...@@ -382,6 +382,32 @@ data before running `pg_basebackup`. ...@@ -382,6 +382,32 @@ data before running `pg_basebackup`.
The replication process is now over. The replication process is now over.
## PGBouncer support (optional)
1. First, enter the PostgreSQL console as an admin user.
2. Then create the read-only user:
```sql
-- NOTE: Use the password defined earlier
CREATE USER gitlab_geo_fdw WITH password 'mypassword';
GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
-- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
-- automatically.
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
```
3. Enter the PostgreSQL console on the secondary tracking database and change the user mapping to this new user:
```
ALTER USER MAPPING FOR gitlab_geo SERVER gitlab_secondary OPTIONS (SET user 'gitlab_geo_fdw')
```
## MySQL replication ## MySQL replication
MySQL replication is not supported for Geo. MySQL replication is not supported for Geo.
......
...@@ -91,6 +91,29 @@ have been resolved to our satisfaction by the relicensing of the reference ...@@ -91,6 +91,29 @@ have been resolved to our satisfaction by the relicensing of the reference
implementations under MIT, and the use of the OWF license for the GraphQL implementations under MIT, and the use of the OWF license for the GraphQL
specification. specification.
## Compatibility Guidelines
The HTTP API is versioned using a single number, the current one being 4. This
number symbolises the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
not explicit. This allows for a stable API endpoint, but also means new
features can be added to the API in the same version number.
New features and bug fixes are released in tandem with a new GitLab, and apart
from incidental patch and security releases, are released on the 22nd each
month. Backward incompatible changes (e.g. endpoints removal, parameters
removal etc.), as well as removal of entire API versions are done in tandem
with a major point release of GitLab itself. All deprecations and changes
between two versions should be listed in the documentation. For the changes
between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
#### Current status
Currently two API versions are available, v3 and v4. v3 is deprecated and
will soon be removed. Deletion is scheduled for
[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
## Basic usage ## Basic usage
API requests should be prefixed with `api` and the API version. The API version API requests should be prefixed with `api` and the API version. The API version
......
...@@ -40,7 +40,6 @@ comments: false ...@@ -40,7 +40,6 @@ comments: false
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid - [Gotchas](gotchas.md) to avoid
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [Avoid modules with instance variables](module_with_instance_variables.md) if possible
- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md) - [How to dump production data to staging](db_dump.md)
- [Working with the GitHub importer](github_importer.md) - [Working with the GitHub importer](github_importer.md)
- [Elasticsearch integration docs](elasticsearch.md) - [Elasticsearch integration docs](elasticsearch.md)
......
# Object state models
## Diagrams
[GitLab object state models](https://drive.google.com/drive/u/3/folders/0B5tDlHAM4iZINmpvYlJXcDVqMGc)
---
## Legend
![legend](img/state-model-legend.png)
---
## Issue
[`app/models/issue.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/issue.rb)
![issue](img/state-model-issue.png)
---
## Merge request
[`app/models/merge_request.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/merge_request.rb)
![merge request](img/state-model-merge-request.png)
\ No newline at end of file
- content_for :merge_access_levels do - content_for :merge_access_levels do
.merge_access_levels-container .merge_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge js-multiselect wide', options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable qa-allowed-to-merge-dropdown capitalize-header', filter: true,
data: { input_id: 'merge_access_levels_attributes', default_label: 'Select' } }) data: { input_id: 'merge_access_levels_attributes', default_label: 'Select' } })
- content_for :push_access_levels do - content_for :push_access_levels do
.push_access_levels-container .push_access_levels-container
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push js-multiselect wide', options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable qa-allowed-to-push-dropdown capitalize-header', filter: true,
data: { input_id: 'push_access_levels_attributes', default_label: 'Select' } }) data: { input_id: 'push_access_levels_attributes', default_label: 'Select' } })
.help-block .help-block
Only groups that Only groups that
......
- can_unprotect = can?(current_user, :update_protected_branch, protected_branch) - can_unprotect = can?(current_user, :update_protected_branch, protected_branch)
%td %td
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: "js-allowed-to-merge", disabled: !can_unprotect } = render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', disabled: !can_unprotect, toggle_class: 'js-allowed-to-merge qa-allowed-to-merge' }
%td %td
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: "js-allowed-to-push", disabled: !can_unprotect } = render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', disabled: !can_unprotect, toggle_class: 'js-allowed-to-push qa-allowed-to-push' }
...@@ -14,14 +14,10 @@ module Gitlab ...@@ -14,14 +14,10 @@ module Gitlab
@command.seeds_block&.call(pipeline) @command.seeds_block&.call(pipeline)
## ##
# Populate pipeline with all stages and builds from pipeline seeds. # Populate pipeline with all stages, and stages with builds.
# #
pipeline.stage_seeds.each do |stage| pipeline.stage_seeds.each do |stage|
pipeline.stages << stage.to_resource pipeline.stages << stage.to_resource
stage.seeds.each do |build|
pipeline.builds << build.to_resource
end
end end
if pipeline.stages.none? if pipeline.stages.none?
......
...@@ -19,6 +19,12 @@ module QA ...@@ -19,6 +19,12 @@ module QA
end end
end end
module Project
module Settings
autoload :ProtectedBranches, 'qa/ee/page/project/settings/protected_branches'
end
end
module MergeRequest module MergeRequest
autoload :Show, 'qa/ee/page/merge_request/show' autoload :Show, 'qa/ee/page/merge_request/show'
end end
......
module QA
module EE
module Page
module Project
module Settings
module ProtectedBranches
def self.prepended(page)
page.module_eval do
view 'ee/app/views/projects/protected_branches/ee/_create_protected_branch.html.haml' do
element :allowed_to_push_select
element :allowed_to_push_dropdown
element :allowed_to_merge_select
element :allowed_to_merge_dropdown
end
view 'ee/app/views/projects/protected_branches/ee/_protected_branch_access_summary.html.haml' do
element :allowed_to_push
element :allowed_to_merge
end
end
end
end
end
end
end
end
end
...@@ -2,7 +2,8 @@ module QA ...@@ -2,7 +2,8 @@ module QA
module Factory module Factory
module Resource module Resource
class Branch < Factory::Base class Branch < Factory::Base
attr_accessor :project, :branch_name, :allow_to_push, :protected attr_accessor :project, :branch_name,
:allow_to_push, :allow_to_merge, :protected
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'protected-branch-project' project.name = 'protected-branch-project'
...@@ -23,6 +24,7 @@ module QA ...@@ -23,6 +24,7 @@ module QA
def initialize def initialize
@branch_name = 'test/branch' @branch_name = 'test/branch'
@allow_to_push = true @allow_to_push = true
@allow_to_merge = true
@protected = false @protected = false
end end
...@@ -63,7 +65,22 @@ module QA ...@@ -63,7 +65,22 @@ module QA
page.allow_no_one_to_push page.allow_no_one_to_push
end end
if allow_to_merge
page.allow_devs_and_masters_to_merge
else
page.allow_no_one_to_merge
end
page.wait(reload: false) do
!page.first('.btn-create').disabled?
end
page.protect_branch page.protect_branch
# Wait for page load, which resets the expanded sections
page.wait(reload: false) do
!page.has_content?('Collapse')
end
end end
end end
end end
......
...@@ -3,6 +3,8 @@ module QA ...@@ -3,6 +3,8 @@ module QA
module Project module Project
module Settings module Settings
class ProtectedBranches < Page::Base class ProtectedBranches < Page::Base
prepend EE::Page::Project::Settings::ProtectedBranches
view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do
element :protected_branch_select element :protected_branch_select
element :protected_branch_dropdown element :protected_branch_dropdown
...@@ -11,6 +13,13 @@ module QA ...@@ -11,6 +13,13 @@ module QA
view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
element :allowed_to_push_select element :allowed_to_push_select
element :allowed_to_push_dropdown element :allowed_to_push_dropdown
element :allowed_to_merge_select
element :allowed_to_merge_dropdown
end
view 'app/views/projects/protected_branches/_update_protected_branch.html.haml' do
element :allowed_to_push
element :allowed_to_merge
end end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
...@@ -30,11 +39,19 @@ module QA ...@@ -30,11 +39,19 @@ module QA
end end
def allow_no_one_to_push def allow_no_one_to_push
allow_to_push('No one') click_allow(:push, 'No one')
end end
def allow_devs_and_masters_to_push def allow_devs_and_masters_to_push
allow_to_push('Developers + Masters') click_allow(:push, 'Developers + Masters')
end
def allow_no_one_to_merge
click_allow(:merge, 'No one')
end
def allow_devs_and_masters_to_merge
click_allow(:merge, 'Developers + Masters')
end end
def protect_branch def protect_branch
...@@ -55,11 +72,15 @@ module QA ...@@ -55,11 +72,15 @@ module QA
private private
def allow_to_push(text) def click_allow(action, text)
click_element :allowed_to_push_select click_element :"allowed_to_#{action}_select"
within_element(:allowed_to_push_dropdown) do within_element(:"allowed_to_#{action}_dropdown") do
click_on text click_on text
wait(reload: false) do
has_css?('.is-active')
end
end end
end end
end end
......
...@@ -19,6 +19,13 @@ module QA ...@@ -19,6 +19,13 @@ module QA
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
end end
after do
# We need to clear localStorage because we're using it for the dropdown,
# and capybara doesn't do this for us.
# https://github.com/teamcapybara/capybara/issues/1702
Capybara.execute_script 'localStorage.clear()'
end
scenario 'user is able to protect a branch' do scenario 'user is able to protect a branch' do
protected_branch = Factory::Resource::Branch.fabricate! do |resource| protected_branch = Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name resource.branch_name = branch_name
......
...@@ -10,13 +10,16 @@ feature 'Dashboard > milestone filter', :js do ...@@ -10,13 +10,16 @@ feature 'Dashboard > milestone filter', :js do
let!(:issue) { create :issue, author: user, project: project, milestone: milestone } let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 } let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
dropdown_toggle_button = '.js-milestone-select'
before do before do
sign_in(user) sign_in(user)
visit issues_dashboard_path(author_id: user.id)
end end
context 'default state' do context 'default state' do
it 'shows issues with Any Milestone' do it 'shows issues with Any Milestone' do
visit issues_dashboard_path(author_id: user.id)
page.all('.issue-info').each do |issue_info| page.all('.issue-info').each do |issue_info|
expect(issue_info.text).to match(/v\d.0/) expect(issue_info.text).to match(/v\d.0/)
end end
...@@ -24,31 +27,51 @@ feature 'Dashboard > milestone filter', :js do ...@@ -24,31 +27,51 @@ feature 'Dashboard > milestone filter', :js do
end end
context 'filtering by milestone' do context 'filtering by milestone' do
milestone_select_selector = '.js-milestone-select'
before do before do
filter_item_select('v1.0', milestone_select_selector) visit issues_dashboard_path(author_id: user.id)
find(milestone_select_selector).click filter_item_select('v1.0', dropdown_toggle_button)
find(dropdown_toggle_button).click
wait_for_requests wait_for_requests
end end
it 'shows issues with Milestone v1.0' do it 'shows issues with Milestone v1.0' do
expect(find('.issues-list')).to have_selector('.issue', count: 1) expect(find('.issues-list')).to have_selector('.issue', count: 1)
expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) expect(find('.milestone-filter .dropdown-content')).to have_selector('a.is-active', count: 1)
end end
it 'should not change active Milestone unless clicked' do it 'should not change active Milestone unless clicked' do
page.within '.milestone-filter' do
expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
# open & close dropdown
find('.dropdown-menu-close').click find('.dropdown-menu-close').click
expect(find('.milestone-filter')).not_to have_selector('.dropdown.open') expect(page).not_to have_selector('.dropdown.open')
find(milestone_select_selector).click find(dropdown_toggle_button).click
expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
expect(find('.dropdown-content a.is-active')).to have_content('v1.0') expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
end end
end end
end
context 'with milestone filter in URL' do
before do
visit issues_dashboard_path(author_id: user.id, milestone_title: milestone.title)
find(dropdown_toggle_button).click
wait_for_requests
end
it 'has milestone selected' do
expect(find('.milestone-filter .dropdown-content')).to have_css('.is-active', text: milestone.title)
end
it 'removes milestone filter from URL after clicking "Any Milestone"' do
expect(current_url).to include("milestone_title=#{milestone.title}")
find('.milestone-filter .dropdown-content li', text: 'Any Milestone').click
expect(current_url).not_to include('milestone_title')
end
end
end end
...@@ -242,4 +242,29 @@ describe BlobHelper do ...@@ -242,4 +242,29 @@ describe BlobHelper do
end end
end end
end end
describe '#ide_edit_path' do
let(:project) { create(:project) }
around do |example|
old_script_name = Rails.application.routes.default_url_options[:script_name]
begin
example.run
ensure
Rails.application.routes.default_url_options[:script_name] = old_script_name
end
end
it 'returns full IDE path' do
Rails.application.routes.default_url_options[:script_name] = nil
expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
end
it 'returns IDE path without relative_url_root' do
Rails.application.routes.default_url_options[:script_name] = "/gitlab"
expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/")
end
end
end end
...@@ -32,12 +32,8 @@ describe('new dropdown component', () => { ...@@ -32,12 +32,8 @@ describe('new dropdown component', () => {
it('renders new file, upload and new directory links', () => { it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file'); expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe( expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
'Upload file', expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
);
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
'New directory',
);
}); });
describe('createNewItem', () => { describe('createNewItem', () => {
...@@ -81,4 +77,18 @@ describe('new dropdown component', () => { ...@@ -81,4 +77,18 @@ describe('new dropdown component', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('dropdownOpen', () => {
it('scrolls dropdown into view', done => {
spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
vm.dropdownOpen = true;
setTimeout(() => {
expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalled();
done();
});
});
});
}); });
...@@ -25,25 +25,17 @@ describe('new file modal component', () => { ...@@ -25,25 +25,17 @@ describe('new file modal component', () => {
it(`sets modal title as ${type}`, () => { it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file'; const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe( expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
`Create new ${title}`,
);
}); });
it(`sets button label as ${type}`, () => { it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file'; const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe( expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
`Create ${title}`,
);
}); });
it(`sets form label as ${type}`, () => { it(`sets form label as ${type}`, () => {
const title = type === 'tree' ? 'Directory' : 'File'; expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe('Name');
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
`${title} name`,
);
}); });
describe('createEntryInStore', () => { describe('createEntryInStore', () => {
......
import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder } from '~/ide/stores/actions'; import actions, {
stageAllChanges,
unstageAllChanges,
toggleFileFinder,
updateTempFlagForEntry,
} from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
...@@ -340,6 +345,49 @@ describe('Multi-file store actions', () => { ...@@ -340,6 +345,49 @@ describe('Multi-file store actions', () => {
}); });
}); });
describe('updateTempFlagForEntry', () => {
it('commits UPDATE_TEMP_FLAG', done => {
const f = {
...file(),
path: 'test',
tempFile: true,
};
store.state.entries[f.path] = f;
testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[],
done,
);
});
it('commits UPDATE_TEMP_FLAG and dispatches for parent', done => {
const parent = {
...file(),
path: 'testing',
};
const f = {
...file(),
path: 'test',
parentPath: 'testing',
};
store.state.entries[parent.path] = parent;
store.state.entries[f.path] = f;
testAction(
updateTempFlagForEntry,
{ file: f, tempFile: false },
store.state,
[{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
[{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }],
done,
);
});
});
describe('toggleFileFinder', () => { describe('toggleFileFinder', () => {
it('commits TOGGLE_FILE_FINDER', done => { it('commits TOGGLE_FILE_FINDER', done => {
testAction( testAction(
......
...@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => { ...@@ -87,6 +87,28 @@ describe('Multi-file store mutations', () => {
}); });
}); });
describe('UPDATE_TEMP_FLAG', () => {
beforeEach(() => {
localState.entries.test = {
...file(),
tempFile: true,
changed: true,
};
});
it('updates tempFile flag', () => {
mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
expect(localState.entries.test.tempFile).toBe(false);
});
it('updates changed flag', () => {
mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
expect(localState.entries.test.changed).toBe(false);
});
});
describe('TOGGLE_FILE_FINDER', () => { describe('TOGGLE_FILE_FINDER', () => {
it('updates fileFindVisible', () => { it('updates fileFindVisible', () => {
mutations.TOGGLE_FILE_FINDER(localState, true); mutations.TOGGLE_FILE_FINDER(localState, true);
......
...@@ -35,11 +35,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ...@@ -35,11 +35,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'populates pipeline with stages' do it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted expect(pipeline.stages.first).not_to be_persisted
end
it 'populates pipeline with builds' do
expect(pipeline.builds).to be_one
expect(pipeline.builds.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted expect(pipeline.stages.first.builds.first).not_to be_persisted
end end
...@@ -151,8 +146,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ...@@ -151,8 +146,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
step.perform! step.perform!
expect(pipeline.stages.size).to eq 1 expect(pipeline.stages.size).to eq 1
expect(pipeline.builds.size).to eq 1 expect(pipeline.stages.first.builds.size).to eq 1
expect(pipeline.builds.first.name).to eq 'rspec' expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
end end
end end
end end
...@@ -947,7 +947,7 @@ describe SystemNoteService do ...@@ -947,7 +947,7 @@ describe SystemNoteService do
it 'sets the note text' do it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200) noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h," expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end end
end end
......
var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus;
PerformanceBar = (function() {
PerformanceBar.prototype.appInfo = null;
PerformanceBar.prototype.width = null;
PerformanceBar.formatTime = function(value) {
if (value >= 1000) {
return ((value / 1000).toFixed(3)) + "s";
} else {
return (value.toFixed(0)) + "ms";
}
};
function PerformanceBar(options) {
var k, v;
if (options == null) {
options = {};
}
this.el = $('#peek-view-performance-bar .performance-bar');
for (k in options) {
v = options[k];
this[k] = v;
}
if (this.width == null) {
this.width = this.el.width();
}
if (this.timing == null) {
this.timing = window.performance.timing;
}
}
PerformanceBar.prototype.render = function(serverTime) {
var networkTime, perfNetworkTime;
if (serverTime == null) {
serverTime = 0;
}
this.el.empty();
this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive');
perfNetworkTime = this.timing.responseEnd - this.timing.requestStart;
if (serverTime && serverTime <= perfNetworkTime) {
networkTime = perfNetworkTime - serverTime;
this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime);
this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo);
} else {
this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd');
}
this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd');
this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd');
this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd');
return this.el;
};
PerformanceBar.prototype.isLoaded = function() {
return this.timing.domInteractive;
};
PerformanceBar.prototype.start = function() {
return this.timing.navigationStart;
};
PerformanceBar.prototype.end = function() {
return this.timing.domInteractive;
};
PerformanceBar.prototype.total = function() {
return this.end() - this.start();
};
PerformanceBar.prototype.addBar = function(name, color, start, end, info) {
var bar, left, offset, time, title, width;
if (typeof start === 'string') {
start = this.timing[start];
}
if (typeof end === 'string') {
end = this.timing[end];
}
if (!((start != null) && (end != null))) {
return;
}
time = end - start;
offset = start - this.start();
left = this.mapH(offset);
width = this.mapH(time);
title = name + ": " + (PerformanceBar.formatTime(time));
bar = $('<li></li>', {
'data-title': title,
'data-toggle': 'tooltip',
'data-container': 'body'
});
bar.css({
width: width + "px",
left: left + "px",
background: color
});
return this.el.append(bar);
};
PerformanceBar.prototype.mapH = function(offset) {
return offset * (this.width / this.total());
};
return PerformanceBar;
})();
renderPerformanceBar = function() {
var bar, resp, span, time;
resp = $('#peek-server_response_time');
time = Math.round(resp.data('time') * 1000);
bar = new PerformanceBar;
bar.render(time);
span = $('<span>', {
'data-toggle': 'tooltip',
'data-title': 'Total navigation time for this page.',
'data-container': 'body'
}).text(PerformanceBar.formatTime(bar.total()));
return updateStatus(span);
};
updateStatus = function(html) {
return $('#serverstats').html(html);
};
ajaxStart = null;
$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) {
return ajaxStart = event.timeStamp;
});
$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) {
var ajaxEnd, serverTime, total;
if (ajaxStart == null) {
return;
}
ajaxEnd = event.timeStamp;
total = ajaxEnd - ajaxStart;
serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0;
return setTimeout(function() {
var bar, now, span, tech;
now = new Date().getTime();
bar = new PerformanceBar({
timing: {
requestStart: ajaxStart,
responseEnd: ajaxEnd,
domLoading: ajaxEnd,
domInteractive: now
},
isLoaded: function() {
return true;
},
start: function() {
return ajaxStart;
},
end: function() {
return now;
}
});
bar.render(serverTime);
if ($.fn.pjax != null) {
tech = 'PJAX';
} else {
tech = 'Turbolinks';
}
span = $('<span>', {
'data-toggle': 'tooltip',
'data-title': tech + " navigation time",
'data-container': 'body'
}).text(PerformanceBar.formatTime(total));
updateStatus(span);
return ajaxStart = null;
}, 0);
});
$(function() {
if (window.performance) {
return renderPerformanceBar();
} else {
return $('#peek-view-performance-bar').remove();
}
});
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment