Commit 0600bde6 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' into ce-to-ee-2018-01-03

parents f7529142 38521d11
......@@ -3,6 +3,7 @@ inherit_gem:
- rubocop-default.yml
inherit_from: .rubocop_todo.yml
require: ./rubocop/rubocop
AllCops:
TargetRailsVersion: 4.2
......
This source diff could not be displayed because it is too large. You can view the blob instead.
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import axios from '../../lib/utils/axios_utils';
import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource);
export default () => {
const el = document.getElementById('js-notebook-viewer');
......@@ -50,14 +48,14 @@ export default () => {
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then(response => response.json())
.then((res) => {
this.json = res;
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status) {
if (e.status !== 200) {
this.loadError = true;
}
......
......@@ -2,7 +2,6 @@
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
......@@ -31,8 +30,6 @@ import collapseIcon from './icons/fullscreen_collapse.svg';
import expandIcon from './icons/fullscreen_expand.svg';
import tooltip from '../vue_shared/directives/tooltip';
Vue.use(VueResource);
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
......@@ -104,9 +101,9 @@ $(() => {
Store.disabled = this.disabled;
gl.boardService.all()
.then(response => response.json())
.then((resp) => {
resp.forEach((board) => {
.then(res => res.data)
.then((data) => {
data.forEach((board) => {
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
......@@ -122,7 +119,9 @@ $(() => {
Store.addPromotionState();
this.loading = false;
})
.catch(() => new Flash('An error occurred. Please try again.'));
.catch(() => {
Flash('An error occurred while fetching the board lists. Please try again.');
});
},
methods: {
updateTokens() {
......@@ -134,7 +133,7 @@ $(() => {
newIssue.setFetchingState('subscriptions', true);
newIssue.setFetchingState('weight', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.json())
.then(res => res.data)
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.setFetchingState('weight', false);
......@@ -177,7 +176,7 @@ $(() => {
if (issue.id === id && issue.sidebarInfoEndpoint) {
issue.setLoadingState('weight', true);
BoardService.updateWeight(issue.sidebarInfoEndpoint, newWeight)
.then(res => res.json())
.then(res => res.data)
.then((data) => {
issue.setLoadingState('weight', false);
issue.updateData({
......
......@@ -65,7 +65,7 @@ export default {
// Save the labels
gl.boardService.generateDefaultLists()
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
data.forEach((listObj) => {
const list = Store.findList('title', listObj.title);
......
......@@ -147,7 +147,7 @@ export default {
});
} else {
gl.boardService.createBoard(this.board)
.then(resp => resp.json())
.then(resp => resp.data)
.then((data) => {
visitUrl(data.board_path);
})
......
......@@ -92,7 +92,7 @@ import BoardForm from './board_form.vue';
if (this.open && !this.boards.length) {
gl.boardService.allBoards()
.then(res => res.json())
.then(res => res.data)
.then((json) => {
this.loading = false;
this.boards = json;
......
......@@ -89,7 +89,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
page: this.page,
per: this.perPage,
}))
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
if (clearIssues) {
this.issues = [];
......
......@@ -40,7 +40,7 @@ class List {
save () {
return gl.boardService.createList(this.label.id)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
this.id = data.id;
this.type = data.list_type;
......@@ -90,7 +90,7 @@ class List {
}
return gl.boardService.getIssuesForList(this.id, data)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
this.loading = false;
this.issuesSize = data.size;
......@@ -108,7 +108,7 @@ class List {
this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
issue.id = data.id;
issue.iid = data.iid;
......
/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
import Vue from 'vue';
import axios from '../../lib/utils/axios_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boardsEndpoint = boardsEndpoint;
this.boardId = boardId;
this.listsEndpoint = listsEndpoint;
this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.bulkUpdatePath = bulkUpdatePath;
}
});
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
generate: {
method: 'POST',
url: `${listsEndpoint}/generate.json`
generateBoardsPath(id) {
return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
}
});
this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
url: bulkUpdatePath,
},
});
generateIssuesPath(id) {
return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
}
allBoards () {
return this.boards.get();
static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
createBoard (board) {
board.label_ids = (board.labels || []).map(b => b.id);
allBoards() {
return axios.get(this.generateBoardsPath());
}
createBoard(board) {
const boardPayload = { ...board };
boardPayload.label_ids = (board.labels || []).map(b => b.id);
if (board.label_ids.length === 0) {
board.label_ids = [''];
if (boardPayload.label_ids.length === 0) {
boardPayload.label_ids = [''];
}
if (board.assignee) {
board.assignee_id = board.assignee.id;
if (boardPayload.assignee) {
boardPayload.assignee_id = boardPayload.assignee.id;
}
if (board.milestone) {
board.milestone_id = board.milestone.id;
if (boardPayload.milestone) {
boardPayload.milestone_id = boardPayload.milestone.id;
}
if (board.id) {
return this.boards.update({ id: board.id }, { board });
if (boardPayload.id) {
return axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload });
}
return this.boards.save({}, { board });
return axios.post(this.generateBoardsPath(), { board: boardPayload });
}
deleteBoard ({ id }) {
return this.boards.delete({ id });
deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id));
}
all () {
return this.lists.get();
all() {
return axios.get(this.listsEndpoint);
}
generateDefaultLists () {
return this.lists.generate({});
generateDefaultLists() {
return axios.post(this.listsEndpointGenerate, {});
}
createList (label_id) {
return this.lists.save({}, {
createList(labelId) {
return axios.post(this.listsEndpoint, {
list: {
label_id
}
label_id: labelId,
},
});
}
updateList (id, position) {
return this.lists.update({ id }, {
updateList(id, position) {
return axios.put(`${this.listsEndpoint}/${id}`, {
list: {
position
}
position,
},
});
}
destroyList (id) {
return this.lists.delete({ id });
destroyList(id) {
return axios.delete(`${this.listsEndpoint}/${id}`);
}
getIssuesForList (id, filter = {}) {
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
return this.issues.get(data);
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
return this.issue.update({ id }, {
from_list_id,
to_list_id,
move_before_id,
move_after_id,
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(BoardService.generateIssuePath(this.boardId, id), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
}
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), {
issue,
});
}
getBacklog(data) {
return this.boards.issues(data);
return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
}
bulkUpdate(issueIds, extraData = {}) {
......@@ -115,23 +113,21 @@ export default class BoardService {
}),
};
return this.issues.bulkUpdate(data);
return axios.post(this.bulkUpdatePath, data);
}
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
return axios.get(endpoint);
}
static updateWeight(endpoint, weight = null) {
return Vue.http.put(endpoint, {
'issue[weight]': weight,
}, {
emulateJSON: true,
return axios.put(endpoint, {
weight,
});
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
return axios.post(endpoint);
}
}
......
......@@ -14,7 +14,8 @@ const unhealthyIcon = 'fa-times';
const unknownIcon = 'fa-times';
const notAvailable = 'Not Available';
const versionMismatch = 'Does not match the primary node version';
const versionMismatchClass = 'geo-node-version-mismatch';
const nodeMismatchClass = 'geo-node-mismatch';
const storageMismatch = 'Does not match the primary storage configuration';
class GeoNodeStatus {
constructor(el) {
......@@ -34,6 +35,7 @@ class GeoNodeStatus {
this.$health = $('.js-health-message', this.$status.parent());
this.$version = $('.js-gitlab-version', this.$status);
this.$secondaryVersion = $('.js-secondary-version', this.$status);
this.$secondaryStorage = $('.js-secondary-storage-shards', this.$status);
this.endpoint = this.$el.data('status-url');
this.$advancedStatus = $('.js-advanced-geo-node-status-toggler', this.$status.parent());
this.$advancedStatus.on('click', GeoNodeStatus.toggleShowAdvancedStatus.bind(this));
......@@ -204,13 +206,23 @@ class GeoNodeStatus {
if (!this.primaryVersion || (this.primaryVersion === status.version
&& this.primaryRevision === status.revision)) {
this.$secondaryVersion.removeClass(`${versionMismatchClass}`);
this.$secondaryVersion.removeClass(`${nodeMismatchClass}`);
this.$secondaryVersion.text(`${status.version} (${status.revision})`);
} else {
this.$secondaryVersion.addClass(`${versionMismatchClass}`);
this.$secondaryVersion.addClass(`${nodeMismatchClass}`);
this.$secondaryVersion.text(`${status.version} (${status.revision}) - ${versionMismatch}`);
}
if (status.storage_shards_match == null) {
this.$secondaryStorage.text('UNKNOWN');
} else if (status.storage_shards_match) {
this.$secondaryStorage.removeClass(`${nodeMismatchClass}`);
this.$secondaryStorage.text('OK');
} else {
this.$secondaryStorage.addClass(`${nodeMismatchClass}`);
this.$secondaryStorage.text(storageMismatch);
}
if (status.repositories_count > 0) {
const repositoriesStats = GeoNodeStatus.getSyncStatistics({
syncedCount: status.repositories_synced_count,
......
......@@ -4,6 +4,7 @@
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import newDropdown from './new_dropdown/index.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
mixins: [
......@@ -13,6 +14,7 @@
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
},
props: {
file: {
......@@ -28,13 +30,6 @@
...mapState([
'leftPanelCollapsed',
]),
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
isSubmodule() {
return this.file.type === 'submodule';
},
......@@ -96,16 +91,18 @@
class="multi-file-table-name"
:colspan="submoduleColSpan"
>
<i
class="fa fa-fw file-icon"
:class="fileIcon"
:style="levelIndentation"
aria-hidden="true"
>
</i>
<a
class="repo-file-name"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="file.type === 'tree'"
:opened="file.opened"
:style="levelIndentation"
:size="16"
>
</file-icon>
{{ file.name }}
<fileStatusIcon
:file="file">
......
<script>
import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
props: {
......@@ -12,6 +13,7 @@ export default {
components: {
fileStatusIcon,
fileIcon,
},
computed: {
......@@ -68,6 +70,11 @@ export default {
:class="{active : tab.active }"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
>
</file-icon>
{{ tab.name }}
<file-status-icon
:file="tab"
......
......@@ -172,8 +172,8 @@ export default {
},
updateIssuable() {
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
return this.service.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
if (location.pathname !== data.web_url) {
......@@ -182,7 +182,7 @@ export default {
return this.service.getData();
})
.then(res => res.json())
.then(res => res.data)
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
......@@ -207,7 +207,7 @@ export default {
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then(res => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
......@@ -225,7 +225,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => res.json().then(data => this.store.updateState(data)),
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
......
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class Service {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/realtime_changes`,
},
});
this.endpoint = `${endpoint}.json`;
this.realtimeEndpoint = `${endpoint}/realtime_changes`;
}
getData() {
return this.resource.realtimeChanges();
return axios.get(this.realtimeEndpoint);
}
deleteIssuable() {
return this.resource.delete();
return axios.delete(this.endpoint);
}
updateIssuable(data) {
return this.resource.update(data);
return axios.put(this.endpoint, data);
}
}
......@@ -232,7 +232,7 @@ export const nodeMatchesSelector = (node, selector) => {
export const normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
Object.keys(headers).forEach((e) => {
Object.keys(headers || {}).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
......
......@@ -18,7 +18,7 @@ export function getParameterValues(sParam) {
// @param {String} url
export function mergeUrlParams(params, url) {
let newUrl = Object.keys(params).reduce((acc, paramName) => {
const paramValue = params[paramName];
const paramValue = encodeURIComponent(params[paramName]);
const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
if (paramValue === null) {
......
......@@ -34,10 +34,10 @@ export default {
if (isConfirmed) {
MRWidgetService.stopEnvironment(deployment.stop_url)
.then(res => res.json())
.then((res) => {
if (res.redirect_url) {
visitUrl(res.redirect_url);
.then(res => res.data)
.then((data) => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
})
.catch(() => {
......
......@@ -102,11 +102,11 @@ export default {
return res;
}
return res.json();
return res.data;
})
.then((res) => {
this.computeGraphData(res.metrics, res.deployment_time);
return res;
.then((data) => {
this.computeGraphData(data.metrics, data.deployment_time);
return data;
})
.catch(() => {
this.loadFailed = true;
......
......@@ -8,7 +8,7 @@ export default {
template: `
<div class="mr-widget-body media">
<div class="space-children">
<status-icon status="failed" />
<status-icon status="warning" />
<button
type="button"
class="btn btn-success btn-sm"
......
......@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" />
<status-icon status="warning" />
<div class="media-body space-children">
<span class="bold">
<template v-if="mr.mergeError">{{mr.mergeError}}.</template>
......
......@@ -12,13 +12,13 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" />
<status-icon status="warning" />
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
:author="mr.closedEvent.author"
:dateTitle="mr.closedEvent.updatedAt"
:dateReadable="mr.closedEvent.formattedUpdatedAt"
:author="mr.metrics.closedBy"
:dateTitle="mr.metrics.closedAt"
:dateReadable="mr.metrics.readableClosedAt"
/>
<section class="mr-info-list">
<p>
......
......@@ -11,7 +11,7 @@ export default {
template: `
<div class="mr-widget-body media">
<status-icon
status="failed"
status="warning"
:show-disabled-button="true" />
<div class="media-body space-children">
<span
......
......@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
......
......@@ -31,9 +31,9 @@ export default {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
......@@ -49,9 +49,9 @@ export default {
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
.then(res => res.json())
.then((res) => {
if (res.status === 'merge_when_pipeline_succeeds') {
.then(res => res.data)
.then((data) => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
......
......@@ -47,9 +47,9 @@ export default {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.json())
.then((res) => {
if (res.message === 'Branch was removed') {
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
......@@ -68,9 +68,9 @@ export default {
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.mergedEvent.author"
:date-title="mr.mergedEvent.updatedAt"
:date-readable="mr.mergedEvent.formattedUpdatedAt" />
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
......
......@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
......
......@@ -68,7 +68,7 @@ export default {
},
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
return 'failed';
return 'warning';
}
return 'success';
},
......@@ -139,16 +139,16 @@ export default {
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.json())
.then((res) => {
const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
.then(res => res.data)
.then((data) => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (res.status === 'merge_when_pipeline_succeeds') {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
} else if (res.status === 'success') {
} else if (data.status === 'success') {
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', res.merge_error);
eventHub.$emit('FailedToMerge', data.merge_error);
}
})
.catch(() => {
......@@ -163,9 +163,9 @@ export default {
},
handleMergePolling(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.json())
.then((res) => {
if (res.state === 'merged') {
.then(res => res.data)
.then((data) => {
if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
......@@ -178,11 +178,11 @@ export default {
// If user checked remove source branch and we didn't remove the branch yet
// we should start another polling for source branch remove process
if (this.removeSourceBranch && res.source_branch_exists) {
if (this.removeSourceBranch && data.source_branch_exists) {
this.initiateRemoveSourceBranchPolling();
}
} else if (res.merge_error) {
eventHub.$emit('FailedToMerge', res.merge_error);
} else if (data.merge_error) {
eventHub.$emit('FailedToMerge', data.merge_error);
stopPolling();
} else {
// MR is not merged yet, continue polling until the state becomes 'merged'
......@@ -203,11 +203,11 @@ export default {
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.json())
.then((res) => {
.then(res => res.data)
.then((data) => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (res.source_branch_exists) {
if (data.source_branch_exists) {
continuePolling();
} else {
// Branch is removed. Update widget, stop polling and hide the spinner
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
......
......@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="true" />
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
......
......@@ -23,9 +23,9 @@ export default {
removeWIP() {
this.isMakingRequest = true;
this.service.removeWIP()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
......@@ -37,7 +37,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
......
......@@ -86,14 +86,14 @@ export default {
},
checkStatus(cb) {
return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
this.mr.setData(res);
.then(res => res.data)
.then((data) => {
this.handleNotification(data);
this.mr.setData(data);
this.setFaviconHelper();
if (cb) {
cb.call(null, res);
cb.call(null, data);
}
})
.catch(() => new Flash('Something went wrong. Please try again.'));
......@@ -124,10 +124,10 @@ export default {
},
fetchDeployments() {
return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
this.mr.deployments = res;
.then(res => res.data)
.then((data) => {
if (data.length) {
this.mr.deployments = data;
}
})
.catch(() => {
......@@ -137,9 +137,9 @@ export default {
fetchActionsContent() {
this.service.fetchMergeActionsContent()
.then((res) => {
if (res.body) {
if (res.data) {
const el = document.createElement('div');
el.innerHTML = res.body;
el.innerHTML = res.data;
document.body.appendChild(el);
Project.initRefSwitcher();
}
......
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
constructor(endpoints) {
this.mergeResource = Vue.resource(endpoints.mergePath);
this.mergeCheckResource = Vue.resource(`${endpoints.statusPath}?serializer=widget`);
this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
this.endpoints = endpoints;
}
merge(data) {
return this.mergeResource.save(data);
return axios.post(this.endpoints.mergePath, data);
}
cancelAutomaticMerge() {
return this.cancelAutoMergeResource.save();
return axios.post(this.endpoints.cancelAutoMergePath);
}
removeWIP() {
return this.removeWIPResource.save();
return axios.post(this.endpoints.removeWIPPath);
}
removeSourceBranch() {
return this.removeSourceBranchResource.delete();
return axios.delete(this.endpoints.sourceBranchPath);
}
fetchDeployments() {
return this.deploymentsResource.get();
return axios.get(this.endpoints.ciEnvironmentsStatusPath);
}
poll() {
return this.pollResource.get();
return axios.get(`${this.endpoints.statusPath}?serializer=basic`);
}
checkStatus() {
return this.mergeCheckResource.get();
return axios.get(`${this.endpoints.statusPath}?serializer=widget`);
}
fetchMergeActionsContent() {
return this.mergeActionsContentResource.get();
return axios.get(this.endpoints.mergeActionsContentPath);
}
static stopEnvironment(url) {
return Vue.http.post(url);
return axios.post(url);
}
static fetchMetrics(metricsUrl) {
return Vue.http.get(`${metricsUrl}.json`);
return axios.get(`${metricsUrl}.json`);
}
}
......@@ -41,9 +41,8 @@ export default class MergeRequestStore {
}
this.updatedAt = data.updated_at;
this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.metrics = MergeRequestStore.buildMetrics(data.metrics);
this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
......@@ -127,42 +126,41 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge;
}
static getEventObject(event) {
static buildMetrics(metrics) {
if (!metrics) {
return {};
}
return {
author: MergeRequestStore.getAuthorObject(event),
updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
mergedBy: MergeRequestStore.formatUserObject(metrics.merged_by),
closedBy: MergeRequestStore.formatUserObject(metrics.closed_by),
mergedAt: formatDate(metrics.merged_at),
closedAt: formatDate(metrics.closed_at),
readableMergedAt: MergeRequestStore.getReadableDate(metrics.merged_at),
readableClosedAt: MergeRequestStore.getReadableDate(metrics.closed_at),
};
}
static getAuthorObject(event) {
if (!event) {
static formatUserObject(user) {
if (!user) {
return {};
}
return {
name: event.author.name || '',
username: event.author.username || '',
webUrl: event.author.web_url || '',
avatarUrl: event.author.avatar_url || '',
name: user.name || '',
username: user.username || '',
webUrl: user.web_url || '',
avatarUrl: user.avatar_url || '',
};
}
static getEventUpdatedAtDate(event) {
if (!event) {
static getReadableDate(date) {
if (!date) {
return '';
}
return event.updated_at;
}
static getEventDate(event) {
const timeagoInstance = new Timeago();
if (!event) {
return '';
}
return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
return timeagoInstance.format(date);
}
}
<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';
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
<file-icon
name="retry"
:size="32"
css-classes="top"
/>
*/
export default {
props: {
fileName: {
type: String,
required: true,
},
folder: {
type: Boolean,
required: false,
default: false,
},
opened: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Boolean,
required: false,
default: false,
},
size: {
type: Number,
required: false,
default: 16,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
components: {
loadingIcon,
icon,
},
computed: {
spriteHref() {
const iconName = getIconForFile(this.fileName) || 'file';
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
// We don't have a open folder icon yet
return this.opened ? 'folder' : 'folder';
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script>
<template>
<span>
<svg
:class="[iconSizeClass, cssClasses]"
v-if="!loading && !folder">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
<icon
v-if="!loading && folder"
:name="folderIconName"
:size="size"
/>
<loading-icon
v-if="loading"
:inline="true"
/>
</span>
</template>
......@@ -127,7 +127,7 @@
}
}
.geo-node-version-mismatch {
.geo-node-mismatch {
color: $gl-danger;
}
......
......@@ -101,8 +101,14 @@
padding: 6px 12px;
}
.multi-file-table-name {
table.table tr td.multi-file-table-name {
width: 350px;
padding: 6px 12px;
svg {
vertical-align: middle;
margin-right: 2px;
}
}
.multi-file-table-col-commit-message {
......@@ -137,6 +143,10 @@
border-bottom: 1px solid $white-dark;
cursor: pointer;
svg {
vertical-align: middle;
}
&.active {
background-color: $white-light;
border-bottom-color: $white-light;
......
......@@ -293,3 +293,7 @@
margin: 0 0 5px 17px;
}
}
.deprecated-service {
cursor: default;
}
......@@ -30,6 +30,13 @@ module IconsHelper
ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
end
def sprite_file_icons_path
# SVG Sprites currently don't work across domains, so in the case of a CDN
# we have to set the current path deliberately to prevent addition of asset_host
sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
end
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?
......
......@@ -29,5 +29,16 @@ module ServicesHelper
"#{event}_events"
end
def service_save_button(service)
button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
end
def disable_fields_service?(service)
!current_controller?("admin/services") && service.deprecated?
end
extend self
end
......@@ -99,7 +99,7 @@ module Issuable
strip_attributes :title
after_save :record_metrics, unless: :imported?
after_save :ensure_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
......@@ -345,11 +345,6 @@ module Issuable
false
end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
end
##
# Override in issuable specialization
#
......@@ -357,6 +352,10 @@ module Issuable
false
end
def ensure_metrics
self.metrics || create_metrics
end
##
# Overriden in MergeRequest
#
......
......@@ -34,6 +34,8 @@ module Storage
# So we basically we mute exceptions in next actions
begin
send_update_instructions
write_projects_repository_config
true
rescue
# Returning false does not rollback after_* transaction but gives
......
......@@ -315,6 +315,11 @@ class Issue < ActiveRecord::Base
private
def ensure_metrics
super
metrics.record!
end
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
......
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
self.merged_at = Time.now
end
self.save
end
belongs_to :latest_closed_by, class_name: 'User'
belongs_to :merged_by, class_name: 'User'
end
......@@ -281,4 +281,11 @@ class Namespace < ActiveRecord::Base
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
def write_projects_repository_config
all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly
project.write_repository_config
end
end
end
......@@ -1426,6 +1426,8 @@ class Project < ActiveRecord::Base
end
def after_rename_repo
write_repository_config
path_before_change = previous_changes['path'].first
# We need to check if project had been rolled out to move resource to hashed storage or not and decide
......@@ -1438,6 +1440,16 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
end
def write_repository_config(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
repo.config['gitlab.fullpath'] = gl_full_path
rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
end
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache
......
......@@ -33,6 +33,7 @@ class KubernetesService < DeploymentService
before_validation :enforce_namespace_to_lower_case
validate :deprecation_validation, unless: :template?
validates :namespace,
allow_blank: true,
length: 1..63,
......@@ -147,6 +148,17 @@ class KubernetesService < DeploymentService
@kubeclient ||= build_kubeclient!
end
def deprecated?
!active
end
def deprecation_message
content = <<-MESSAGE.strip_heredoc
Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
MESSAGE
content.html_safe
end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
......@@ -228,4 +240,20 @@ class KubernetesService < DeploymentService
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
def deprecation_validation
return if active_changed?(from: true, to: false)
if deprecated?
errors[:base] << deprecation_message
end
end
def deprecated_message_content
if active?
"Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
else
"Fields on this page are now uneditable, you can configure"
end
end
end
......@@ -269,6 +269,14 @@ class Service < ActiveRecord::Base
service
end
def deprecated?
false
end
def deprecation_message
nil
end
private
def cache_project_has_external_issue_tracker
......
# This is an in-memory structure only. The repository storage configuration is
# in gitlab.yml and not in the database. This model makes it easier to work
# with the configuration.
class StorageShard
include ActiveModel::Model
attr_accessor :name, :path
validates :name, presence: true
validates :path, presence: true
# Generates an array of StorageShard objects from the currrent storage
# configuration using the gitlab.yml array of key/value pairs:
#
# {"default"=>{"path"=>"/home/git/repositories", ...}
#
# The key is the shard name, and the values are the parameters for that shard.
def self.all
Settings.repositories.storages.map do |name, params|
config = params.symbolize_keys.merge(name: name)
config.slice!(*allowed_params)
StorageShard.new(config)
end
end
def self.allowed_params
%i(name path).freeze
end
end
......@@ -812,10 +812,7 @@ class User < ActiveRecord::Base
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless projects_limit.nil? || connection_default_value_defined
return unless has_attribute?(:projects_limit) && projects_limit.nil?
self.projects_limit = current_application_settings.default_projects_limit
end
......
class EventEntity < Grape::Entity
expose :author, using: UserEntity
expose :updated_at
end
class MergeRequestMetricsEntity < Grape::Entity
expose :latest_closed_at, as: :closed_at
expose :merged_at
expose :latest_closed_by, as: :closed_by, using: UserEntity
expose :merged_by, using: UserEntity
end
......@@ -35,9 +35,11 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).approvals_path
end
# Events
expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity
expose :metrics do |merge_request|
metrics = build_metrics(merge_request)
MergeRequestMetricsEntity.new(metrics).as_json
end
# User entities
expose :merge_user, using: UserEntity
......@@ -196,4 +198,27 @@ class MergeRequestWidgetEntity < IssuableEntity
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
end
# Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
# we can remove this method and just serialize MergeRequest#metrics
# instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
def build_metrics(merge_request)
# There's no need to query and serialize metrics data for merge requests that are not
# merged or closed.
return unless merge_request.merged? || merge_request.closed?
return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
build_metrics_from_events(merge_request)
end
def build_metrics_from_events(merge_request)
closed_event = merge_request.closed_event
merge_event = merge_request.merge_event
MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
latest_closed_by: closed_event&.author,
merged_at: merge_event&.updated_at,
merged_by: merge_event&.author)
end
end
......@@ -103,6 +103,6 @@ class EventCreateService
author_id: current_user.id
)
Event.create(attributes)
Event.create!(attributes)
end
end
class MergeRequestMetricsService
delegate :update!, to: :@merge_request_metrics
def initialize(merge_request_metrics)
@merge_request_metrics = merge_request_metrics
end
def merge(event)
update!(merged_by_id: event.author_id, merged_at: event.created_at)
end
def close(event)
update!(latest_closed_by_id: event.author_id, latest_closed_at: event.created_at)
end
def reopen
update!(latest_closed_by_id: nil, latest_closed_at: nil)
end
end
......@@ -26,6 +26,10 @@ module MergeRequests
private
def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)
......
......@@ -8,7 +8,7 @@ module MergeRequests
merge_request.allow_broken = true
if merge_request.close
event_service.close_mr(merge_request, current_user)
create_event(merge_request)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
......@@ -19,5 +19,16 @@ module MergeRequests
merge_request
end
private
def create_event(merge_request)
# Making sure MergeRequest::Metrics updates are in sync with
# Event creation.
Event.transaction do
close_event = event_service.close_mr(merge_request, current_user)
merge_request_metrics_service(merge_request).close(close_event)
end
end
end
end
......@@ -9,7 +9,7 @@ module MergeRequests
close_issues(merge_request)
todo_service.merge_merge_request(merge_request, current_user)
merge_request.mark_as_merged
create_merge_event(merge_request, current_user)
create_event(merge_request)
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
......@@ -34,5 +34,14 @@ module MergeRequests
def create_merge_event(merge_request, current_user)
EventCreateService.new.merge_mr(merge_request, current_user)
end
def create_event(merge_request)
# Making sure MergeRequest::Metrics updates are in sync with
# Event creation.
Event.transaction do
merge_event = create_merge_event(merge_request, current_user)
merge_request_metrics_service(merge_request).merge(merge_event)
end
end
end
end
......@@ -4,7 +4,7 @@ module MergeRequests
return merge_request unless can?(current_user, :update_merge_request, merge_request)
if merge_request.reopen
event_service.reopen_mr(merge_request, current_user)
create_event(merge_request)
create_note(merge_request, 'reopened')
notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
......@@ -16,5 +16,16 @@ module MergeRequests
merge_request
end
private
def create_event(merge_request)
# Making sure MergeRequest::Metrics updates are in sync with
# Event creation.
Event.transaction do
event_service.reopen_mr(merge_request, current_user)
merge_request_metrics_service(merge_request).reopen
end
end
end
end
......@@ -94,6 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
@project.write_repository_config
@project.create_wiki unless skip_wiki?
create_services_from_active_templates(@project)
......
......@@ -29,7 +29,9 @@ module Projects
result &&= move_repository("#{@old_wiki_disk_path}", "#{@new_disk_path}.wiki")
end
unless result
if result
project.write_repository_config
else
rollback_folder_move
project.storage_version = nil
end
......
......@@ -77,6 +77,8 @@ module Projects
project.old_path_with_namespace = @old_path
project.expires_full_path_cache
write_repository_config(@new_path)
execute_system_hooks
end
rescue Exception # rubocop:disable Lint/RescueException
......@@ -100,6 +102,10 @@ module Projects
project.save!
end
def write_repository_config(full_path)
project.write_repository_config(gl_full_path: full_path)
end
def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
......@@ -112,6 +118,7 @@ module Projects
def rollback_side_effects
rollback_folder_move
update_namespace_and_visibility(@old_namespace)
write_repository_config(@old_path)
end
def rollback_folder_move
......
.flash-container.flash-container-page
.flash-alert.deprecated-service
%span= @service.deprecation_message
......@@ -13,10 +13,7 @@
= render 'shared/service_settings', form: form, subject: @service
- if @service.editable?
.footer-block.row-content-block
%button.btn.btn-save{ type: 'submit' }
= icon('spinner spin', class: 'hidden js-btn-spinner')
%span.js-btn-label
Save changes
= service_save_button(@service)
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
......
......@@ -2,4 +2,6 @@
- page_title @service.title, "Services"
- add_to_breadcrumbs("Settings", edit_project_path(@project))
= render 'deprecated_message' if @service.deprecation_message
= render 'form'
......@@ -7,6 +7,7 @@
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
- disabled = disable_fields_service?(@service)
.form-group
- if type == "password" && value.present?
......@@ -15,14 +16,14 @@
= form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder, required: required
= form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
- elsif type == 'textarea'
= form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
= form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
- elsif type == 'checkbox'
= form.check_box name
= form.check_box name, disabled: disabled
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled}
- elsif type == 'password'
= form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required
= form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled
- if help
%span.help-block= help
......@@ -11,7 +11,7 @@
.form-group
= form.label :active, "Active", class: "control-label"
.col-sm-10
= form.check_box :active
= form.check_box :active, disabled: disable_fields_service?(@service)
- if @service.supported_events.present?
.form-group
......
......@@ -104,6 +104,7 @@
- cronjob:geo_metrics_update
- cronjob:geo_prune_event_log
- cronjob:geo_repository_sync
- cronjob:geo_sidekiq_cron_config
- cronjob:historical_data
- cronjob:ldap_all_groups_sync
- cronjob:ldap_sync
......
---
title: Allow sidekiq to react to becoming a Geo primary or secondary without a restart
merge_request: 3878
author:
type: changed
---
title: Fix a bug where branch could not be delete due to a push rule config
merge_request: 3900
author:
type: fixed
---
title: Change MR widget failed icons to warning icons
merge_request: 3669
author:
type: changed
---
title: Check if shard configuration is same across Geo nodes
merge_request:
author:
type: added
---
title: Record EE instances without a license correctly in usage ping
merge_request:
author:
type: fixed
---
title: Add some extra fields to Geo API node and status
merge_request: 3858
author:
type: added
---
title: More descriptive error when clocks between Geo nodes are out of sync
merge_request: 3860
author:
type: changed
---
title: User#projects_limit remove DB default and added NOT NULL constraint
merge_request: 16165
author: Mario de la Ossa
type: fixed
---
title: Fix some POST/DELETE requests in IE by switching some bundles to Axios for Ajax requests
merge_request: 15951
author:
type: fixed
---
title: Disable creation of new Kubernetes Integrations unless they're active or created
from template
merge_request: 41054
author:
type: added
---
title: Handle GitLab hashed storage repositories using the repo import task
merge_request:
author:
type: added
---
title: Cache merged and closed events data in merge_request_metrics table
merge_request:
author:
type: performance
class AddEventsRelatedColumnsToMergeRequestMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
change_table :merge_request_metrics do |t|
t.references :merged_by, references: :users
t.references :latest_closed_by, references: :users
end
add_column :merge_request_metrics, :latest_closed_at, :datetime_with_timezone
add_concurrent_foreign_key :merge_request_metrics, :users,
column: :merged_by_id,
on_delete: :nullify
add_concurrent_foreign_key :merge_request_metrics, :users,
column: :latest_closed_by_id,
on_delete: :nullify
end
def down
if foreign_keys_for(:merge_request_metrics, :merged_by_id).any?
remove_foreign_key :merge_request_metrics, column: :merged_by_id
end
if foreign_keys_for(:merge_request_metrics, :latest_closed_by_id).any?
remove_foreign_key :merge_request_metrics, column: :latest_closed_by_id
end
remove_columns :merge_request_metrics,
:merged_by_id, :latest_closed_by_id, :latest_closed_at
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ChangeUserProjectLimitNotNullAndRemoveDefault < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def up
# Set Users#projects_limit to NOT NULL and remove the default value
change_column_null :users, :projects_limit, false
change_column_default :users, :projects_limit, nil
end
def down
change_column_null :users, :projects_limit, true
change_column_default :users, :projects_limit, 10
end
end
# frozen_string_literal: true
# rubocop:disable GitlabSecurity/SqlInjection
class SchedulePopulateMergeRequestMetricsWithEventsData < ActiveRecord::Migration
DOWNTIME = false
BATCH_SIZE = 10_000
MIGRATION = 'PopulateMergeRequestMetricsWithEventsData'
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
self.table_name = 'merge_requests'
include ::EachBatch
end
def up
merge_requests = MergeRequest.where("id IN (#{updatable_merge_requests_union_sql})").reorder(:id)
say 'Scheduling `PopulateMergeRequestMetricsWithEventsData` jobs'
# It will update around 4_000_000 records in batches of 10_000 merge
# requests (running between 10 minutes) and should take around 66 hours to complete.
# Apparently, production PostgreSQL is able to vacuum 10k-20k dead_tuples by
# minute, and at maximum, each of these jobs should UPDATE 20k records.
#
# More information about the updates in `PopulateMergeRequestMetricsWithEventsData` class.
#
merge_requests.each_batch(of: BATCH_SIZE) do |relation, index|
range = relation.pluck('MIN(id)', 'MAX(id)').first
BackgroundMigrationWorker.perform_in(index * 10.minutes, MIGRATION, range)
end
end
def down
execute "update merge_request_metrics set latest_closed_at = null"
execute "update merge_request_metrics set latest_closed_by_id = null"
execute "update merge_request_metrics set merged_by_id = null"
end
private
# On staging:
# Planning time: 0.682 ms
# Execution time: 22033.158 ms
#
def updatable_merge_requests_union_sql
metrics_not_exists_clause =
'NOT EXISTS (SELECT 1 FROM merge_request_metrics WHERE merge_request_metrics.merge_request_id = merge_requests.id)'
without_metrics_data = <<-SQL.strip_heredoc
merge_request_metrics.merged_by_id IS NULL OR
merge_request_metrics.latest_closed_by_id IS NULL OR
merge_request_metrics.latest_closed_at IS NULL
SQL
mrs_without_metrics_record = MergeRequest
.where(metrics_not_exists_clause)
.select(:id)
mrs_without_events_data = MergeRequest
.joins('INNER JOIN merge_request_metrics ON merge_requests.id = merge_request_metrics.merge_request_id')
.where(without_metrics_data)
.select(:id)
Gitlab::SQL::Union.new([mrs_without_metrics_record, mrs_without_events_data]).to_sql
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171220191323) do
ActiveRecord::Schema.define(version: 20171229225929) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1402,6 +1402,9 @@ ActiveRecord::Schema.define(version: 20171220191323) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "pipeline_id"
t.integer "merged_by_id"
t.integer "latest_closed_by_id"
t.datetime_with_timezone "latest_closed_at"
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
......@@ -2289,7 +2292,7 @@ ActiveRecord::Schema.define(version: 20171220191323) do
t.datetime "updated_at"
t.string "name"
t.boolean "admin", default: false, null: false
t.integer "projects_limit", default: 10
t.integer "projects_limit", null: false
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
......@@ -2528,6 +2531,8 @@ ActiveRecord::Schema.define(version: 20171220191323) do
add_foreign_key "merge_request_diffs", "merge_requests", name: "fk_8483f3258f", on_delete: :cascade
add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
......
......@@ -22,6 +22,7 @@ Example response:
"url": "https://primary.example.com/",
"primary": true,
"enabled": true,
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
"clone_protocol": "http"
......@@ -31,6 +32,7 @@ Example response:
"url": "https://secondary.example.com/",
"primary": false,
"enabled": true,
"current": false,
"files_max_capacity": 10,
"repos_max_capacity": 25,
"clone_protocol": "http"
......@@ -56,6 +58,7 @@ Example response:
"url": "https://primary.example.com/",
"primary": true,
"enabled": true,
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
"clone_protocol": "http"
......@@ -80,6 +83,8 @@ Example response:
"geo_node_id": 2,
"healthy": true,
"health": "Healthy",
"health_status": "Healthy",
"missing_oauth_application": false,
"attachments_count": 1,
"attachments_synced_count": 1,
"attachments_failed_count": 0,
......@@ -125,6 +130,8 @@ Example response:
"geo_node_id": 2,
"healthy": true,
"health": "Healthy",
"health_status": "Healthy",
"missing_oauth_application": false,
"attachments_count": 1,
"attachments_synced_count": 1,
"attachments_failed_count": 0,
......
......@@ -562,6 +562,9 @@ DELETE /projects/:id/services/jira
Kubernetes / Openshift integration
CAUTION: **Warning:**
Kubernetes service integration has been deprecated in GitLab 10.3. API service endpoints will continue to work as long as the Kubernetes service is active, however if the service is inactive API endpoints will automatically return a `400 Bad Request`. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
### Create/Edit Kubernetes service
Set Kubernetes service for a project.
......
......@@ -273,6 +273,12 @@ primary before the database is replicated.
secondary node. Leave blank to replicate all. Read more in
[selective replication](#selective-replication).
1. Click the **Add node** button.
1. SSH into your GitLab **primary** server and login as root to verify the
secondary is reachable:
```
gitlab-rake gitlab:geo:check
```
The new secondary geo node will have the status **Unhealthy**. This is expected
because we have not yet configured the secondary server. This is the next step.
......@@ -371,6 +377,13 @@ because we have not yet configured the secondary server. This is the next step.
timedatectl status | grep 'NTP synchronized'
```
1. Verify the secondary if configured correctly and that the primary is
reachable:
```
gitlab-rake gitlab:geo:check
```
### Step 4. Initiate the replication process
Below we provide a script that connects the database on the secondary node to
......
......@@ -2,6 +2,9 @@
last_updated: 2017-09-25
---
CAUTION: **Warning:**
Kubernetes service integration has been deprecated in GitLab 10.3. If the service is active the cluster information still be editable, however we advised to disable and reconfigure the clusters using the new [Clusters](../clusters/index.md) page. If the service is inactive the fields will be uneditable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
# GitLab Kubernetes / OpenShift integration
GitLab can be configured to interact with Kubernetes, or other systems using the
......
......@@ -40,7 +40,7 @@ Click on the service links to see further configuration instructions and details
| [JIRA](jira.md) | JIRA issue tracker |
| [Jenkins](../../../integration/jenkins.md) | An extendable open source continuous integration server |
| JetBrains TeamCity CI | A continuous integration and build server |
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Kubernetes](kubernetes.md) _(Has been deprecated in GitLab 10.3)_ | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
......
......@@ -28,7 +28,7 @@ export default {
computed: {
status() {
if (this.mr.approvals.approvals_left > 0) {
return 'failed';
return 'warning';
}
return 'success';
},
......
......@@ -32,7 +32,7 @@
return 'loading';
}
if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) {
return 'failed';
return 'warning';
}
return 'success';
},
......
......@@ -12,7 +12,7 @@ export default {
},
template: `
<div class="media">
<status-icon status="failed" showDisabledButton />
<status-icon status="warning" showDisabledButton />
<div class="media-body">
<span class="bold">
Merge requests are read-only in a secondary Geo node
......
......@@ -40,6 +40,8 @@ module EE
render_bad_geo_auth('Bad token')
rescue ::Gitlab::Geo::InvalidDecryptionKeyError
render_bad_geo_auth("Invalid decryption key")
rescue ::Gitlab::Geo::InvalidSignatureTimeError
render_bad_geo_auth("Invalid signature time ")
end
def render_bad_geo_auth(message)
......
......@@ -31,14 +31,6 @@ module EE
refs.map { |sha| commit(sha.strip) }
end
def push_remote_branches(remote, branches)
gitlab_shell.push_remote_branches(repository_storage_path, disk_path, remote, branches)
end
def delete_remote_branches(remote, branches)
gitlab_shell.delete_remote_branches(repository_storage_path, disk_path, remote, branches)
end
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
......
......@@ -4,6 +4,7 @@ class GeoNodeStatus < ActiveRecord::Base
# Whether we were successful in reaching this node
attr_accessor :success, :version, :revision
attr_writer :health_status
attr_accessor :storage_shards
# Be sure to keep this consistent with Prometheus naming conventions
PROMETHEUS_METRICS = {
......@@ -49,12 +50,12 @@ class GeoNodeStatus < ActiveRecord::Base
def self.from_json(json_data)
json_data.slice!(*allowed_params)
GeoNodeStatus.new(json_data)
GeoNodeStatus.new(HashWithIndifferentAccess.new(json_data))
end
def self.allowed_params
excluded_params = %w(id created_at updated_at).freeze
extra_params = %w(success health health_status last_event_timestamp cursor_last_event_timestamp version revision).freeze
extra_params = %w(success health health_status last_event_timestamp cursor_last_event_timestamp version revision storage_shards).freeze
self.column_names - excluded_params + extra_params
end
......@@ -74,6 +75,7 @@ class GeoNodeStatus < ActiveRecord::Base
self.lfs_objects_count = lfs_objects_finder.count_lfs_objects
self.attachments_count = attachments_finder.count_attachments
self.last_successful_status_check_at = Time.now
self.storage_shards = StorageShard.all
if Gitlab::Geo.primary?
self.replication_slots_count = geo_node.replication_slots_count
......@@ -152,12 +154,40 @@ class GeoNodeStatus < ActiveRecord::Base
calc_percentage(replication_slots_count, replication_slots_used_count)
end
# This method only is useful when the storage shard information is loaded
# from a remote node via JSON.
def storage_shards_match?
return unless Gitlab::Geo.primary?
shards_match?(current_shards, primary_shards)
end
def [](key)
public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end
private
def current_shards
serialize_storage_shards(storage_shards)
end
def primary_shards
serialize_storage_shards(StorageShard.all)
end
def serialize_storage_shards(shards)
StorageShardSerializer.new.represent(shards).as_json
end
def shards_match?(first, second)
sort_by_name(first) == sort_by_name(second)
end
def sort_by_name(shards)
shards.sort_by { |shard| shard['name'] }
end
def attachments_finder
@attachments_finder ||= Geo::AttachmentRegistryFinder.new(current_node: geo_node)
end
......
......@@ -81,6 +81,10 @@ class RemoteMirror < ActiveRecord::Base
update_status == 'started'
end
def update_repository(options)
raw.update(options)
end
def sync
return unless enabled?
return if Gitlab::Geo.secondary?
......@@ -144,6 +148,10 @@ class RemoteMirror < ActiveRecord::Base
private
def raw
@raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, ref_name)
end
def recently_scheduled?
return false unless self.last_update_started_at
......
......@@ -8,6 +8,7 @@ class GeoNodeStatusEntity < Grape::Entity
node.healthy? ? 'Healthy' : node.health
end
expose :health_status
expose :missing_oauth_application, as: :missing_oauth_application
expose :attachments_count
expose :attachments_synced_count
......@@ -58,12 +59,31 @@ class GeoNodeStatusEntity < Grape::Entity
expose :namespaces, using: NamespaceEntity
# We load GeoNodeStatus data in two ways:
#
# 1. Directly by asking a Geo node via an API call
# 2. Via cached state in the database
#
# We don't yet cached the state of the shard information in the database, so if
# we don't have this information omit from the serialization entirely.
expose :storage_shards, using: StorageShardEntity, if: ->(status, options) do
status.storage_shards.present?
end
expose :storage_shards_match?, as: :storage_shards_match, if: -> (status, options) do
Gitlab::Geo.primary? && status.storage_shards.present?
end
private
def namespaces
object.geo_node.namespaces
end
def missing_oauth_application
object.geo_node.missing_oauth_application?
end
def version
Gitlab::VERSION
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment