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:
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *except-docs
<<: *pull-cache
dependencies:
- setup-test-env
......@@ -152,6 +152,10 @@ stages:
variables:
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
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
......@@ -310,7 +314,7 @@ stages:
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
SETUP_DB: "false"
......@@ -351,12 +355,12 @@ stages:
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate:reset
.migration-paths: &migration-paths
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
CREATE_DB_USER: "true"
script:
......@@ -768,7 +772,7 @@ migration:path-mysql:
<<: *use-mysql
.db-rollback: &db-rollback
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
......@@ -781,10 +785,9 @@ db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
db:rollback-pg-geo: &db-rollback
db:rollback-pg-geo:
<<: *db-rollback
<<: *use-pg
<<: *except-docs
script:
- bundle exec rake geo:db:migrate VERSION=20170627195211
- bundle exec rake geo:db:migrate
......@@ -799,7 +802,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
dependencies: []
variables:
NODE_ENV: "production"
......@@ -820,7 +823,7 @@ gitlab:assets:compile:
- webpack-report/
karma:
<<: *dedicated-no-docs-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
......@@ -944,7 +947,7 @@ coverage:
- coverage/assets/
lint:javascript:report:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test
dependencies:
- compile-assets
......
......@@ -25,12 +25,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [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)
- [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)
- [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)
- [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 triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
......@@ -128,7 +128,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, 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"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
......@@ -172,13 +172,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
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.
Assigning a team label makes sure issues get the attention of the appropriate
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".
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
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
\ No newline at end of file
......@@ -295,7 +295,6 @@ gem 'batch-loader', '~> 1.2.1'
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
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-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
......
......@@ -632,8 +632,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
......@@ -1167,7 +1165,6 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
......
......@@ -26,11 +26,18 @@ export default {
required: false,
default: false,
},
forceModifiedIcon: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
changedIcon() {
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() {
return `${this.changedIcon}-solid`;
......
<script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
export default {
export default {
components: {
icon,
newModal,
......@@ -27,10 +27,15 @@
dropdownOpen: false,
};
},
watch: {
dropdownOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView();
});
},
},
methods: {
...mapActions([
'createTempEntry',
]),
...mapActions(['createTempEntry']),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
......@@ -43,7 +48,7 @@
this.dropdownOpen = !this.dropdownOpen;
},
},
};
};
</script>
<template>
......@@ -71,7 +76,10 @@
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<ul
class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
>
<li>
<a
href="#"
......
......@@ -40,13 +40,6 @@ export default {
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
......@@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
<label class="label-light col-sm-3 ide-new-modal-label">
{{ __('Name') }}
</label>
<div class="col-sm-9">
<input
......
......@@ -97,7 +97,7 @@ export default {
:file="file"
/>
</span>
<span class="pull-right">
<span class="pull-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
......@@ -106,7 +106,8 @@ export default {
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
class="prepend-top-5 pull-right"
:force-modified-icon="true"
class="pull-right"
/>
</span>
<new-dropdown
......
......@@ -84,6 +84,7 @@ export default {
<changed-file-icon
v-else
:file="tab"
:force-modified-icon="true"
/>
</button>
......
......@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const toggleRightPanelCollapsed = (
{ dispatch, state },
e = undefined,
) => {
export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
if (e) {
$(e.currentTarget)
.tooltip('hide')
......@@ -77,7 +74,7 @@ export const createTempEntry = (
}
worker.addEventListener('message', ({ data }) => {
const { file } = data;
const { file, parentPath } = data;
worker.terminate();
......@@ -93,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path);
}
if (parentPath && !state.entries[parentPath].opened) {
commit(types.TOGGLE_TREE_OPEN, parentPath);
}
resolve(file);
});
......@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, 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) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
......
......@@ -63,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
.getFileData(file.url)
.getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
......
......@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = (
{ 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}`, {
content: file.content,
changed: !!changedFile,
......
......@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_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';
......@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
import { sortTree } from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
......@@ -73,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree),
tree: sortTree(foundEntry.tree.concat(tree)),
});
}
......@@ -86,10 +87,16 @@ export default {
if (!foundEntry) {
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) {
Object.assign(state, {
viewer,
......
......@@ -33,6 +33,7 @@ export const dataStructure = () => ({
raw: '',
content: '',
parentTreeUrl: '',
parentPath: '',
renderError: false,
base64: false,
editorRow: 1,
......@@ -65,6 +66,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
parentPath = '',
} = entity;
return {
......@@ -81,6 +83,7 @@ export const decorateData = entity => {
opened,
active,
parentTreeUrl,
parentPath,
changed,
renderError,
content,
......@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
};
......
......@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = [];
let file;
let parentPath;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
......@@ -17,6 +18,8 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath];
if (!foundEntry) {
parentPath = parentFolder ? parentFolder.path : null;
const tree = decorateData({
projectId,
branchId,
......@@ -29,6 +32,7 @@ self.addEventListener('message', e => {
tempFile,
changed: tempFile,
opened: tempFile,
parentPath,
});
Object.assign(acc, {
......@@ -52,6 +56,8 @@ self.addEventListener('message', e => {
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
parentPath = fileFolder ? fileFolder.path : null;
file = decorateData({
projectId,
branchId,
......@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content,
base64,
previewMode: viewerInformationForPath(blobName),
parentPath,
});
Object.assign(acc, {
......@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries,
treeList: sortTree(treeList),
file,
parentPath,
});
});
......@@ -12,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
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);
......@@ -26,7 +27,10 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
let milestoneLinkNoneTemplate,
milestoneLinkTemplate,
selectedMilestone,
selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
......@@ -46,45 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestoneDefault = showAny ? '' : null;
selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
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>';
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: (term, callback) => axios.get(milestonesUrl)
.then(({ data }) => {
data: (term, callback) =>
axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
id: null,
name: null,
title: 'Any Milestone',
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
title: 'No Milestone',
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
title: 'Upcoming',
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
title: 'Started',
});
}
if (extraOptions.length) {
......@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`,
filterable: true,
search: {
fields: ['title']
fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
......@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title),
id: (milestone) => {
id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
......@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: (e) => {
opened: e => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
......@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
clicked: clickEvent => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
......@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
const isMRIndex = page === page && page === 'projects:merge_requests:index';
const isSelecting = selected.name !== selectedMilestone;
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();
return;
}
......@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
gl.issueBoards.boardStoreIssueSet(
'milestone',
new ListMilestone({
id: selected.id,
title: selected.name
}));
title: selected.name,
}),
);
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
......@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown');
$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(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
......@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return axios.put(issueUpdateURL, data)
return axios
.put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
......@@ -215,7 +229,10 @@ export default class MilestoneSelect {
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
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')
.text(data.milestone.title);
} else {
......@@ -230,7 +247,7 @@ export default class MilestoneSelect {
$loading.fadeOut();
});
}
}
},
});
});
}
......
......@@ -52,16 +52,15 @@
text() {
const keepContributionsText = s__(`AdminArea|
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.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
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.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
......
......@@ -188,11 +188,11 @@ export default class ActivityCalendar {
},
{
text: 'W',
y: 29 + this.dayYPos(2),
y: 29 + this.dayYPos(3),
},
{
text: 'F',
y: 29 + this.dayYPos(3),
y: 29 + this.dayYPos(5),
},
];
this.svg
......
......@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
......@@ -14,7 +13,6 @@ export default {
detailedMetric,
requestSelector,
simpleMetric,
upstreamPerformanceBar,
},
props: {
store: {
......@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }}
</span>
</div>
<upstream-performance-bar
v-if="initialRequest && currentRequest.details"
/>
<detailed-metric
v-for="metric in $options.detailedMetrics"
: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 performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
......
<script>
import getIconForFile from './file_icon/file_icon_map';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import getIconForFile from './file_icon/file_icon_map';
import loadingIcon from '../../vue_shared/components/loading_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
Sample configuration:
......@@ -15,7 +15,7 @@
/>
*/
export default {
export default {
components: {
loadingIcon,
icon,
......@@ -68,7 +68,7 @@
return this.size ? `s${this.size}` : '';
},
},
};
};
</script>
<template>
<span>
......@@ -82,6 +82,7 @@
v-if="!loading && folder"
:name="folderIconName"
:size="size"
css-classes="folder-icon"
/>
<loading-icon
v-if="loading"
......
......@@ -23,6 +23,7 @@
.fork-svg {
margin-right: 4px;
vertical-align: bottom;
}
}
......
......@@ -70,7 +70,7 @@
}
.branch-info .commit-icon {
margin-right: 3px;
margin-right: 8px;
svg {
top: 3px;
......
......@@ -55,6 +55,7 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
line-height: 22px;
svg {
vertical-align: middle;
......@@ -67,6 +68,11 @@
}
}
.ide-file-icon-holder {
display: flex;
align-items: center;
}
.ide-file-changed-icon {
margin-left: auto;
......@@ -77,7 +83,6 @@
.ide-new-btn {
display: none;
margin-bottom: -4px;
margin-right: -8px;
}
......@@ -90,12 +95,10 @@
}
}
&.folder {
svg {
.folder-icon {
fill: $gl-text-color-secondary;
}
}
}
a {
color: $gl-text-color;
......@@ -111,6 +114,7 @@
.file-col-commit-message {
display: flex;
overflow: visible;
align-items: center;
padding: 6px 12px;
}
......@@ -438,7 +442,7 @@
.projects-sidebar {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
.context-header {
width: auto;
......@@ -967,3 +971,7 @@
background: transparent;
resize: none;
}
.ide-new-modal-label {
line-height: 34px;
}
@import 'framework/variables';
@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof';
#js-peek {
......
......@@ -17,7 +17,7 @@ module BlobHelper
end
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
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
......
......@@ -63,7 +63,7 @@ module CommitsHelper
# Returns a link formatted as a commit branch link
def commit_branch_link(url, text)
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
......
......@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
"changed time estimate to #{parsed_time},"
"changed time estimate to #{parsed_time}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
......@@ -49,7 +49,7 @@
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'false' }, type: 'button' }
delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
%li
......@@ -58,5 +58,5 @@
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: user.name,
delete_contributions: 'true' }, type: 'button' }
delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
......@@ -194,7 +194,7 @@
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: @user.name,
delete_contributions: 'false' }, type: 'button' }
delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
......@@ -226,7 +226,7 @@
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name,
delete_contributions: 'true' }, type: 'button' }
delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
- else
%p
......
......@@ -5,8 +5,3 @@
peek_url: peek_routes.results_url,
profile_url: url_for(params.merge(lineprofiler: 'true')) },
class: Peek.env }
#peek-view-performance-bar.hidden
= render_server_response_time
%span#serverstats
%ul.performance-bar
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header',
options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select wide',
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' }})
- content_for :push_access_levels do
.push_access_levels-container
......
%td
= 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') ,
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 }})
%td
= 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) }
Peek.into Peek::Views::Host
Peek.into Peek::Views::PerformanceBar
if Gitlab::Database.mysql?
require 'peek-mysql2'
......
......@@ -201,7 +201,7 @@ The following guide assumes that:
for more information.
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
gitlab-ctl reconfigure
......@@ -555,6 +555,55 @@ the instructions below:
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 is not supported for Geo.
......
......@@ -382,6 +382,32 @@ data before running `pg_basebackup`.
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 is not supported for Geo.
......
......@@ -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
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
API requests should be prefixed with `api` and the API version. The API version
......
......@@ -40,7 +40,6 @@ comments: false
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
- [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)
- [Working with the GitHub importer](github_importer.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
.merge_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select js-multiselect wide',
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' } })
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push js-multiselect wide',
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true,
options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select js-multiselect wide',
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' } })
.help-block
Only groups that
......
- can_unprotect = can?(current_user, :update_protected_branch, protected_branch)
%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
= 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
@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.stages << stage.to_resource
stage.seeds.each do |build|
pipeline.builds << build.to_resource
end
end
if pipeline.stages.none?
......
......@@ -19,6 +19,12 @@ module QA
end
end
module Project
module Settings
autoload :ProtectedBranches, 'qa/ee/page/project/settings/protected_branches'
end
end
module MergeRequest
autoload :Show, 'qa/ee/page/merge_request/show'
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
module Factory
module Resource
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|
project.name = 'protected-branch-project'
......@@ -23,6 +24,7 @@ module QA
def initialize
@branch_name = 'test/branch'
@allow_to_push = true
@allow_to_merge = true
@protected = false
end
......@@ -63,7 +65,22 @@ module QA
page.allow_no_one_to_push
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
# Wait for page load, which resets the expanded sections
page.wait(reload: false) do
!page.has_content?('Collapse')
end
end
end
end
......
......@@ -3,6 +3,8 @@ module QA
module Project
module Settings
class ProtectedBranches < Page::Base
prepend EE::Page::Project::Settings::ProtectedBranches
view 'app/views/projects/protected_branches/shared/_dropdown.html.haml' do
element :protected_branch_select
element :protected_branch_dropdown
......@@ -11,6 +13,13 @@ module QA
view 'app/views/projects/protected_branches/_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 'app/views/projects/protected_branches/_update_protected_branch.html.haml' do
element :allowed_to_push
element :allowed_to_merge
end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
......@@ -30,11 +39,19 @@ module QA
end
def allow_no_one_to_push
allow_to_push('No one')
click_allow(:push, 'No one')
end
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
def protect_branch
......@@ -55,11 +72,15 @@ module QA
private
def allow_to_push(text)
click_element :allowed_to_push_select
def click_allow(action, text)
click_element :"allowed_to_#{action}_select"
within_element(:allowed_to_push_dropdown) do
within_element(:"allowed_to_#{action}_dropdown") do
click_on text
wait(reload: false) do
has_css?('.is-active')
end
end
end
end
......
......@@ -19,6 +19,13 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
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
protected_branch = Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
......
......@@ -10,13 +10,16 @@ feature 'Dashboard > milestone filter', :js do
let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
dropdown_toggle_button = '.js-milestone-select'
before do
sign_in(user)
visit issues_dashboard_path(author_id: user.id)
end
context 'default state' do
it 'shows issues with Any Milestone' do
visit issues_dashboard_path(author_id: user.id)
page.all('.issue-info').each do |issue_info|
expect(issue_info.text).to match(/v\d.0/)
end
......@@ -24,31 +27,51 @@ feature 'Dashboard > milestone filter', :js do
end
context 'filtering by milestone' do
milestone_select_selector = '.js-milestone-select'
before do
filter_item_select('v1.0', milestone_select_selector)
find(milestone_select_selector).click
visit issues_dashboard_path(author_id: user.id)
filter_item_select('v1.0', dropdown_toggle_button)
find(dropdown_toggle_button).click
wait_for_requests
end
it 'shows issues with Milestone v1.0' do
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
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)
# open & close dropdown
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 a.is-active')).to have_content('v1.0')
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
......@@ -242,4 +242,29 @@ describe BlobHelper do
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
......@@ -32,12 +32,8 @@ describe('new dropdown component', () => {
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')[1].textContent.trim()).toBe(
'Upload file',
);
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
'New directory',
);
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
......@@ -81,4 +77,18 @@ describe('new dropdown component', () => {
.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', () => {
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
`Create new ${title}`,
);
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
`Create ${title}`,
);
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
const title = type === 'tree' ? 'Directory' : 'File';
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
`${title} name`,
);
expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe('Name');
});
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 * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
......@@ -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', () => {
it('commits TOGGLE_FILE_FINDER', done => {
testAction(
......
......@@ -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', () => {
it('updates fileFindVisible', () => {
mutations.TOGGLE_FILE_FINDER(localState, true);
......
......@@ -35,11 +35,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one
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.first).not_to be_persisted
end
......@@ -151,8 +146,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
step.perform!
expect(pipeline.stages.size).to eq 1
expect(pipeline.builds.size).to eq 1
expect(pipeline.builds.first.name).to eq 'rspec'
expect(pipeline.stages.first.builds.size).to eq 1
expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
end
end
end
......@@ -947,7 +947,7 @@ describe SystemNoteService do
it 'sets the note text' do
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
......
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