Commit b94daa35 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into michel.engelen/gitlab-ce-issue/55953

parents 83330822 f90a7601

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis /doc/ @axil @marcia @eread @mikelewis
# Frontend maintainers should see everything in `app/assets/` # Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya *.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
# Someone from the database team should review changes in `db/` # Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS db/ @abrandl @NikolayS
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.use-pg: &use-pg .use-pg: &use-pg
services: services:
- name: postgres:9.6 - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
DOCKER_HOST: tcp://docker:2375 DOCKER_HOST: tcp://docker:2375
script: script:
- node --version - node --version
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache - retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache --prefer-offline
- free -m - free -m
- retry bundle exec rake gitlab:assets:compile - retry bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image - time scripts/build_assets_image
...@@ -82,7 +82,7 @@ gitlab:assets:compile pull-cache: ...@@ -82,7 +82,7 @@ gitlab:assets:compile pull-cache:
stage: prepare stage: prepare
script: script:
- node --version - node --version
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache - retry yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- free -m - free -m
- retry bundle exec rake gitlab:assets:compile - retry bundle exec rake gitlab:assets:compile
- scripts/clean-old-cached-assets - scripts/clean-old-cached-assets
...@@ -231,7 +231,7 @@ qa:selectors: ...@@ -231,7 +231,7 @@ qa:selectors:
before_script: [] before_script: []
script: script:
- date - date
- yarn install --frozen-lockfile --cache-folder .yarn-cache - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- date - date
- yarn run webpack-prod - yarn run webpack-prod
......
.use-pg: &use-pg .use-pg: &use-pg
services: services:
- name: postgres:9.6 - name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine - name: redis:alpine
...@@ -245,7 +245,7 @@ migration:path-pg: ...@@ -245,7 +245,7 @@ migration:path-pg:
.db-rollback: &db-rollback .db-rollback: &db-rollback
extends: .dedicated-no-docs-and-no-qa-pull-cache-job extends: .dedicated-no-docs-and-no-qa-pull-cache-job
script: script:
- bundle exec rake db:migrate VERSION=20170523121229 - bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true - bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
dependencies: dependencies:
- setup-test-env - setup-test-env
......
...@@ -236,5 +236,5 @@ danger-review: ...@@ -236,5 +236,5 @@ danger-review:
script: script:
- git version - git version
- node --version - node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- danger --fail-on-errors=true - danger --fail-on-errors=true
<script>
import { mapGetters } from 'vuex';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
UserAvatarLink,
},
props: {
hasForm: {
type: Boolean,
required: false,
default: false,
},
renderReplyPlaceholder: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters({
currentUser: 'getUserData',
userCanReply: 'userCanReply',
}),
},
};
</script>
<template>
<div class="discussion-reply-holder d-flex clearfix">
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
<user-avatar-link
:link-href="currentUser.path"
:img-src="currentUser.avatar_url"
:img-alt="currentUser.name"
:img-size="40"
class="d-none d-sm-block"
/>
<reply-placeholder
class="qa-discussion-reply"
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
/>
</template>
</template>
<note-signed-out-widget v-else />
</div>
</template>
...@@ -80,7 +80,6 @@ export default { ...@@ -80,7 +80,6 @@ export default {
v-show="isExpanded(discussion)" v-show="isExpanded(discussion)"
:discussion="discussion" :discussion="discussion"
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
:line="line" :line="line"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
......
...@@ -151,7 +151,11 @@ export default { ...@@ -151,7 +151,11 @@ export default {
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false); stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
}, },
methods: { methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']), ...mapActions('diffs', [
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
handleToggleFile(e, checkTarget) { handleToggleFile(e, checkTarget) {
if ( if (
!checkTarget || !checkTarget ||
...@@ -165,7 +169,7 @@ export default { ...@@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage'); this.$emit('showForkMessage');
}, },
handleToggleDiscussions() { handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile); this.toggleFileDiscussionWrappers(this.diffFile);
}, },
handleFileNameClick(e) { handleFileNameClick(e) {
const isLinkToOtherPage = const isLinkToOtherPage =
......
<script> <script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility'; import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
...@@ -19,11 +18,13 @@ export default { ...@@ -19,11 +18,13 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
discussionsExpanded: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
discussionsExpanded() {
return this.discussions.every(discussion => discussion.expanded);
},
allDiscussions() { allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []); return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
}, },
...@@ -45,26 +46,14 @@ export default { ...@@ -45,26 +46,14 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleDiscussion']),
getTooltipText(noteData) { getTooltipText(noteData) {
let { note } = noteData; let { note } = noteData;
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) { if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP); note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
} }
return `${noteData.author.name}: ${note}`; return `${noteData.author.name}: ${note}`;
}, },
toggleDiscussions() {
const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
forceExpanded,
});
});
},
}, },
}; };
</script> </script>
...@@ -76,7 +65,7 @@ export default { ...@@ -76,7 +65,7 @@ export default {
type="button" type="button"
:aria-label="__('Show comments')" :aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button" class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions" @click="$emit('toggleLineDiscussions')"
> >
<icon :size="12" name="collapse" /> <icon :size="12" name="collapse" />
</button> </button>
...@@ -87,7 +76,7 @@ export default { ...@@ -87,7 +76,7 @@ export default {
:img-src="note.author.avatar_url" :img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)" :tooltip-text="getTooltipText(note)"
class="diff-comment-avatar js-diff-comment-avatar" class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions" @click.native="$emit('toggleLineDiscussions')"
/> />
<span <span
v-if="moreText" v-if="moreText"
...@@ -97,7 +86,7 @@ export default { ...@@ -97,7 +86,7 @@ export default {
data-container="body" data-container="body"
data-placement="top" data-placement="top"
role="button" role="button"
@click="toggleDiscussions" @click="$emit('toggleLineDiscussions')"
>+{{ moreCount }}</span >+{{ moreCount }}</span
> >
</template> </template>
......
...@@ -105,7 +105,13 @@ export default { ...@@ -105,7 +105,13 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), ...mapActions('diffs', [
'loadMoreLines',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
'toggleLineDiscussionWrappers',
]),
handleCommentButton() { handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
}, },
...@@ -184,7 +190,14 @@ export default { ...@@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)" @click="setHighlightedRow(lineCode)"
> >
</a> </a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> <diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
"
/>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import diffDiscussions from './diff_discussions.vue'; import { mapActions } from 'vuex';
import diffLineNoteForm from './diff_line_note_form.vue'; import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default { export default {
components: { components: {
diffDiscussions, DiffDiscussions,
diffLineNoteForm, DiffLineNoteForm,
DiffDiscussionReply,
}, },
props: { props: {
line: { line: {
...@@ -32,10 +35,12 @@ export default { ...@@ -32,10 +35,12 @@ export default {
if (!this.line.discussions || !this.line.discussions.length) { if (!this.line.discussions || !this.line.discussions.length) {
return false; return false;
} }
return this.line.discussionsExpanded;
return this.line.discussions.every(discussion => discussion.expanded);
}, },
}, },
methods: {
...mapActions('diffs', ['showCommentForm']),
},
}; };
</script> </script>
...@@ -49,13 +54,22 @@ export default { ...@@ -49,13 +54,22 @@ export default {
:discussions="line.discussions" :discussions="line.discussions"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
<diff-line-note-form <diff-discussion-reply
v-if="line.hasForm" :has-form="line.hasForm"
:diff-file-hash="diffFileHash" :render-reply-placeholder="Boolean(line.discussions.length)"
:line="line" @showNewDiscussionForm="
:note-target-line="line" showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
:help-page-path="helpPagePath" "
/> >
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</template>
</diff-discussion-reply>
</div> </div>
</td> </td>
</tr> </tr>
......
<script> <script>
import diffDiscussions from './diff_discussions.vue'; import { mapActions } from 'vuex';
import diffLineNoteForm from './diff_line_note_form.vue'; import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default { export default {
components: { components: {
diffDiscussions, DiffDiscussions,
diffLineNoteForm, DiffLineNoteForm,
DiffDiscussionReply,
}, },
props: { props: {
line: { line: {
...@@ -29,24 +32,30 @@ export default { ...@@ -29,24 +32,30 @@ export default {
computed: { computed: {
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded) ? this.line.left.discussionsExpanded
: false; : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded) ? this.line.right.discussionsExpanded
: false; : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft; return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return ( return (
this.line.right && this.line.right &&
this.line.right.discussions && this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight && this.hasExpandedDiscussionOnRight &&
this.line.right.type this.line.right.type
); );
...@@ -81,6 +90,22 @@ export default { ...@@ -81,6 +90,22 @@ export default {
return hasCommentFormOnLeft || hasCommentFormOnRight; return hasCommentFormOnLeft || hasCommentFormOnRight;
}, },
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash });
},
}, },
}; };
</script> </script>
...@@ -90,37 +115,49 @@ export default { ...@@ -90,37 +115,49 @@ export default {
<td class="notes-content parallel old" colspan="2"> <td class="notes-content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content"> <div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions <diff-discussions
v-if="line.left.discussions.length"
:discussions="line.left.discussions" :discussions="line.left.discussions"
:line="line.left" :line="line.left"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
</div> </div>
<diff-line-note-form <diff-discussion-reply
v-if="showLeftSideCommentForm" :has-form="showLeftSideCommentForm"
:diff-file-hash="diffFileHash" :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
:line="line.left" @showNewDiscussionForm="showNewDiscussionForm"
:note-target-line="line.left" >
:help-page-path="helpPagePath" <template #form>
line-position="left" <diff-line-note-form
/> :diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td> </td>
<td class="notes-content parallel new" colspan="2"> <td class="notes-content parallel new" colspan="2">
<div v-if="shouldRenderDiscussionsOnRight" class="content"> <div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions <diff-discussions
v-if="line.right.discussions.length"
:discussions="line.right.discussions" :discussions="line.right.discussions"
:line="line.right" :line="line.right"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
/> />
</div> </div>
<diff-line-note-form <diff-discussion-reply
v-if="showRightSideCommentForm" :has-form="showRightSideCommentForm"
:diff-file-hash="diffFileHash" :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
:line="line.right" @showNewDiscussionForm="showNewDiscussionForm"
:note-target-line="line.right" >
line-position="right" <template #form>
/> <diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td> </td>
</tr> </tr>
</template> </template>
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
getNoteFormData, getNoteFormData,
convertExpandLines, convertExpandLines,
idleCallback, idleCallback,
allDiscussionWrappersExpanded,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
...@@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = ( ...@@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions, discussions = rootState.notes.discussions,
) => { ) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions discussions
.filter(discussion => discussion.diff_discussion) .filter(discussion => discussion.diff_discussion)
...@@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = ( ...@@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion, discussion,
diffPositionByLineCode, diffPositionByLineCode,
hash,
}); });
}); });
...@@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { ...@@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id }); commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
}; };
export const toggleLineDiscussions = ({ commit }, options) => {
commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId); const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
...@@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { ...@@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
}); });
}; };
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions;
if (diff.highlighted_diff_lines) {
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
}
if (diff.parallel_diff_lines) {
linesWithDiscussions = diff.parallel_diff_lines.filter(
line =>
(line.left && line.left.discussions.length) ||
(line.right && line.right.discussions.length),
);
}
if (linesWithDiscussions.length) {
linesWithDiscussions.forEach(line => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
});
});
}
};
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({ const postData = getNoteFormData({
commit: state.commit, commit: state.commit,
...@@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { ...@@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true }) return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true })) .then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true })) .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); .catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
}; };
......
...@@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE ...@@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER'; export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
addContextLines, addContextLines,
prepareDiffData, prepareDiffData,
isDiscussionApplicableToLine, isDiscussionApplicableToLine,
updateLineInFile,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -109,7 +110,7 @@ export default { ...@@ -109,7 +110,7 @@ export default {
})); }));
}, },
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state; const { latestDiff } = state;
const discussionLineCode = discussion.line_code; const discussionLineCode = discussion.line_code;
...@@ -130,13 +131,27 @@ export default { ...@@ -130,13 +131,27 @@ export default {
: [], : [],
}); });
const setDiscussionsExpanded = line => {
const isLineNoteTargeted = line.discussions.some(
disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
);
return {
...line,
discussionsExpanded:
line.discussions && line.discussions.length
? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted
: false,
};
};
state.diffFiles = state.diffFiles.map(diffFile => { state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) { if (diffFile.file_hash === fileHash) {
const file = { ...diffFile }; const file = { ...diffFile };
if (file.highlighted_diff_lines) { if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
lineCheck(line) ? mapDiscussions(line) : line, setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
); );
} }
...@@ -148,8 +163,10 @@ export default { ...@@ -148,8 +163,10 @@ export default {
if (left || right) { if (left || right) {
return { return {
...line, ...line,
left: line.left ? mapDiscussions(line.left) : null, left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null, right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
}; };
} }
...@@ -173,32 +190,11 @@ export default { ...@@ -173,32 +190,11 @@ export default {
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) { if (selectedFile) {
if (selectedFile.parallel_diff_lines) { updateLineInFile(selectedFile, lineCode, line =>
const targetLine = selectedFile.parallel_diff_lines.find( Object.assign(line, {
line => discussions: line.discussions.filter(discussion => discussion.notes.length),
(line.left && line.left.line_code === lineCode) || }),
(line.right && line.right.line_code === lineCode), );
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.discussions && selectedFile.discussions.length) { if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter( selectedFile.discussions = selectedFile.discussions.filter(
...@@ -207,6 +203,15 @@ export default { ...@@ -207,6 +203,15 @@ export default {
} }
} }
}, },
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, { discussionsExpanded: expanded }),
);
},
[types.TOGGLE_FOLDER_OPEN](state, path) { [types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened; state.treeEntries[path].opened = !state.treeEntries[path].opened;
}, },
......
...@@ -454,3 +454,48 @@ export const convertExpandLines = ({ ...@@ -454,3 +454,48 @@ export const convertExpandLines = ({
}; };
export const idleCallback = cb => requestIdleCallback(cb); export const idleCallback = cb => requestIdleCallback(cb);
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
updateFn(targetLine[side]);
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
updateFn(targetInlineLine);
}
}
};
export const allDiscussionWrappersExpanded = diff => {
const discussionsExpandedArray = [];
if (diff.parallel_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.left && line.left.discussions.length) {
discussionsExpandedArray.push(line.left.discussionsExpanded);
}
if (line.right && line.right.discussions.length) {
discussionsExpandedArray.push(line.right.discussionsExpanded);
}
});
} else if (diff.highlighted_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.discussions.length) {
discussionsExpandedArray.push(line.discussionsExpanded);
}
});
}
return discussionsExpandedArray.every(el => el);
};
import $ from 'jquery'; import $ from 'jquery';
import { slugifyWithHyphens } from './lib/utils/text_utility'; import { slugify } from './lib/utils/text_utility';
export default class Group { export default class Group {
constructor() { constructor() {
...@@ -14,7 +14,7 @@ export default class Group { ...@@ -14,7 +14,7 @@ export default class Group {
} }
update() { update() {
const slug = slugifyWithHyphens(this.groupName.val()); const slug = slugify(this.groupName.val());
this.groupPath.val(slug); this.groupPath.val(slug);
} }
......
...@@ -107,7 +107,8 @@ export default { ...@@ -107,7 +107,8 @@ export default {
@click="openFileInEditor" @click="openFileInEditor"
> >
<span class="multi-file-commit-list-file-path d-flex align-items-center"> <span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" />{{ file.name }} <file-icon :file-name="file.name" class="append-right-8" />
{{ file.name }}
</span> </span>
<div class="ml-auto d-flex align-items-center"> <div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon"> <div class="d-flex align-items-center ide-commit-list-changed-icon">
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span class="vertical-align-middle">Open in file view</span> <span class="vertical-align-middle">{{ __('Open in file view') }}</span>
<icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" /> <icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" />
</a> </a>
</div> </div>
......
...@@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants'; ...@@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue'; import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
export default { export default {
components: { components: {
...@@ -160,7 +161,14 @@ export default { ...@@ -160,7 +161,14 @@ export default {
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch(err => { .catch(err => {
flash('Error setting up editor. Please try again.', 'alert', document, null, false, true); flash(
__('Error setting up editor. Please try again.'),
'alert',
document,
null,
false,
true,
);
throw err; throw err;
}); });
}, },
...@@ -247,12 +255,8 @@ export default { ...@@ -247,12 +255,8 @@ export default {
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })" @click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
> >
<template v-if="viewer === $options.viewerTypes.edit"> <template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template>
{{ __('Edit') }} <template v-else>{{ __('Review') }}</template>
</template>
<template v-else>
{{ __('Review') }}
</template>
</a> </a>
</li> </li>
<li v-if="file.previewMode" :class="previewTabCSS"> <li v-if="file.previewMode" :class="previewTabCSS">
...@@ -260,9 +264,8 @@ export default { ...@@ -260,9 +264,8 @@ export default {
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })" @click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
>{{ file.previewMode.previewTitle }}</a
> >
{{ file.previewMode.previewTitle }}
</a>
</li> </li>
</ul> </ul>
<external-link :file="file" /> <external-link :file="file" />
......
<script> <script>
import { __, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
...@@ -18,7 +19,9 @@ export default { ...@@ -18,7 +19,9 @@ export default {
}, },
computed: { computed: {
lockTooltip() { lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`; return sprintf(__(`Locked by %{fileLockUserName}`), {
fileLockUserName: this.file.file_lock.user.name,
});
}, },
}, },
}; };
......
<script> <script>
import { __, sprintf } from '~/locale';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
...@@ -27,9 +28,9 @@ export default { ...@@ -27,9 +28,9 @@ export default {
computed: { computed: {
closeLabel() { closeLabel() {
if (this.fileHasChanged) { if (this.fileHasChanged) {
return `${this.tab.name} changed`; return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
} }
return `Close ${this.tab.name}`; return sprintf(__(`Close %{tabname}`, { tabname: this.tab.name }));
}, },
showChangedIcon() { showChangedIcon() {
if (this.tab.pending) return true; if (this.tab.pending) return true;
......
...@@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : ...@@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' :
export const dasherize = str => str.replace(/[_\s]+/g, '-'); export const dasherize = str => str.replace(/[_\s]+/g, '-');
/** /**
* Replaces whitespaces with hyphens and converts to lower case * Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters
* @param {String} str * @param {String} str
* @returns {String} * @returns {String}
*/ */
export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-'); export const slugify = str => {
const slug = str
.trim()
.toLowerCase()
.replace(/[^a-zA-Z0-9_.-]+/g, '-');
return slug === '-' ? '' : slug;
};
/** /**
* Replaces whitespaces with underscore and converts to lower case * Replaces whitespaces with underscore and converts to lower case
......
...@@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) => ...@@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
const initManualOrdering = () => { const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering'); const issueList = document.querySelector('.manual-ordering');
if (!issueList || !(gon.features && gon.features.manualSorting)) { if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) {
return; return;
} }
......
...@@ -39,15 +39,23 @@ export default { ...@@ -39,15 +39,23 @@ export default {
</script> </script>
<template> <template>
<div class="discussion-with-resolve-btn clearfix"> <div class="discussion-with-resolve-btn">
<reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" /> <reply-placeholder
:button-text="s__('MergeRequests|Reply...')"
<div class="btn-group discussion-actions" role="group"> class="qa-discussion-reply"
<resolve-discussion-button @onClick="$emit('showReplyForm')"
v-if="discussion.resolvable" />
:is-resolving="isResolving" <resolve-discussion-button
:button-title="resolveButtonTitle" v-if="discussion.resolvable"
@onClick="$emit('resolve')" :is-resolving="isResolving"
:button-title="resolveButtonTitle"
@onClick="$emit('resolve')"
/>
<div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group">
<resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" />
<jump-to-next-discussion-button
v-if="shouldShowJumpToNextDiscussion"
@onClick="$emit('jumpToNextDiscussion')"
/> />
<resolve-with-issue-button <resolve-with-issue-button
v-if="discussion.resolvable && resolveWithIssuePath" v-if="discussion.resolvable && resolveWithIssuePath"
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { SYSTEM_NOTE } from '../constants'; import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import NoteableNote from './noteable_note.vue'; import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue'; import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue'; import NoteEditedText from './note_edited_text.vue';
...@@ -72,6 +72,7 @@ export default { ...@@ -72,6 +72,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleDiscussion']),
componentName(note) { componentName(note) {
if (note.isPlaceholderNote) { if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) { if (note.placeholderType === SYSTEM_NOTE) {
...@@ -101,7 +102,7 @@ export default { ...@@ -101,7 +102,7 @@ export default {
<component <component
:is="componentName(firstNote)" :is="componentName(firstNote)"
:note="componentData(firstNote)" :note="componentData(firstNote)"
:line="line" :line="line || diffLine"
:commit="commit" :commit="commit"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:show-reply-button="userCanReply" :show-reply-button="userCanReply"
...@@ -118,23 +119,29 @@ export default { ...@@ -118,23 +119,29 @@ export default {
/> />
<slot slot="avatar-badge" name="avatar-badge"></slot> <slot slot="avatar-badge" name="avatar-badge"></slot>
</component> </component>
<toggle-replies-widget <div
v-if="hasReplies" :class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
:collapsed="!isExpanded" >
:replies="replies" <toggle-replies-widget
@toggle="$emit('toggleDiscussion')" v-if="hasReplies"
/> :collapsed="!isExpanded"
<template v-if="isExpanded"> :replies="replies"
<component :class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
:is="componentName(note)" @toggle="toggleDiscussion({ discussionId: discussion.id })"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handleDeleteNote="$emit('deleteNote')"
/> />
</template> <template v-if="isExpanded">
<component
:is="componentName(note)"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handleDeleteNote="$emit('deleteNote')"
/>
</template>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template> </template>
<template v-else> <template v-else>
<component <component
...@@ -148,8 +155,8 @@ export default { ...@@ -148,8 +155,8 @@ export default {
> >
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component> </component>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template> </template>
</ul> </ul>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'ReplyPlaceholder', name: 'ReplyPlaceholder',
props: {
buttonText: {
type: String,
required: true,
},
},
}; };
</script> </script>
...@@ -12,6 +18,6 @@ export default { ...@@ -12,6 +18,6 @@ export default {
:title="s__('MergeRequests|Add a reply')" :title="s__('MergeRequests|Add a reply')"
@click="$emit('onClick')" @click="$emit('onClick')"
> >
{{ s__('MergeRequests|Reply...') }} {{ buttonText }}
</button> </button>
</template> </template>
...@@ -132,7 +132,7 @@ export default { ...@@ -132,7 +132,7 @@ export default {
return this.discussion.diff_discussion && this.renderDiffFile; return this.discussion.diff_discussion && this.renderDiffFile;
}, },
shouldGroupReplies() { shouldGroupReplies() {
return !this.shouldRenderDiffs && !this.discussion.diff_discussion; return !this.shouldRenderDiffs;
}, },
wrapperComponent() { wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div'; return this.shouldRenderDiffs ? diffWithNote : 'div';
...@@ -248,6 +248,11 @@ export default { ...@@ -248,6 +248,11 @@ export default {
clearDraft(this.autosaveKey); clearDraft(this.autosaveKey);
}, },
saveReply(noteText, form, callback) { saveReply(noteText, form, callback) {
if (!noteText) {
this.cancelReplyForm();
callback();
return;
}
const postData = { const postData = {
in_reply_to_discussion_id: this.discussion.reply_id, in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType, target_type: this.getNoteableData.targetType,
...@@ -361,7 +366,6 @@ Please check your network connection and try again.`; ...@@ -361,7 +366,6 @@ Please check your network connection and try again.`;
:line="line" :line="line"
:should-group-replies="shouldGroupReplies" :should-group-replies="shouldGroupReplies"
@startReplying="showReplyForm" @startReplying="showReplyForm"
@toggleDiscussion="toggleDiscussionHandler"
@deleteNote="deleteNoteHandler" @deleteNote="deleteNoteHandler"
> >
<slot slot="avatar-badge" name="avatar-badge"></slot> <slot slot="avatar-badge" name="avatar-badge"></slot>
...@@ -374,7 +378,7 @@ Please check your network connection and try again.`; ...@@ -374,7 +378,7 @@ Please check your network connection and try again.`;
<div <div
v-else-if="showReplies" v-else-if="showReplies"
:class="{ 'is-replying': isReplying }" :class="{ 'is-replying': isReplying }"
class="discussion-reply-holder" class="discussion-reply-holder clearfix"
> >
<user-avatar-link <user-avatar-link
v-if="!isReplying && userCanReply" v-if="!isReplying && userCanReply"
......
...@@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) => ...@@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
.then(res => res.json()) .then(res => res.json())
.then(discussions => { .then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.SET_INITIAL_DISCUSSIONS, discussions);
dispatch('updateResolvableDiscussonsCounts'); dispatch('updateResolvableDiscussionsCounts');
}); });
export const updateDiscussion = ({ commit, state }, discussion) => { export const updateDiscussion = ({ commit, state }, discussion) => {
...@@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) => ...@@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) =>
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget'); dispatch('updateMergeRequestWidget');
dispatch('updateResolvableDiscussonsCounts'); dispatch('updateResolvableDiscussionsCounts');
if (isInMRPage()) { if (isInMRPage()) {
dispatch('diffs/removeDiscussionsFromDiff', discussion); dispatch('diffs/removeDiscussionsFromDiff', discussion);
...@@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi ...@@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi
dispatch('updateMergeRequestWidget'); dispatch('updateMergeRequestWidget');
dispatch('startTaskList'); dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts'); dispatch('updateResolvableDiscussionsCounts');
} else { } else {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
} }
...@@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => ...@@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget'); dispatch('updateMergeRequestWidget');
dispatch('startTaskList'); dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts'); dispatch('updateResolvableDiscussionsCounts');
} }
return res; return res;
}); });
...@@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, ...@@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res); commit(mutationType, res);
dispatch('updateResolvableDiscussonsCounts'); dispatch('updateResolvableDiscussionsCounts');
dispatch('updateMergeRequestWidget'); dispatch('updateMergeRequestWidget');
}); });
...@@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) => ...@@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) =>
}), }),
); );
export const updateResolvableDiscussonsCounts = ({ commit }) => export const updateResolvableDiscussionsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS); commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = ( export const submitSuggestion = (
......
...@@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji'; ...@@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji';
import detailedMetric from './detailed_metric.vue'; import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue'; import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export default { export default {
components: { components: {
detailedMetric, detailedMetric,
requestSelector, requestSelector,
simpleMetric,
}, },
props: { props: {
store: { store: {
...@@ -43,8 +41,13 @@ export default { ...@@ -43,8 +41,13 @@ export default {
details: 'details', details: 'details',
keys: ['feature', 'request'], keys: ['feature', 'request'],
}, },
{
metric: 'redis',
header: 'Redis calls',
details: 'details',
keys: ['cmd'],
},
], ],
simpleMetrics: ['redis'],
data() { data() {
return { currentRequestId: '' }; return { currentRequestId: '' };
}, },
...@@ -124,12 +127,6 @@ export default { ...@@ -124,12 +127,6 @@ export default {
</button> </button>
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a> <a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
</div> </div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:key="metric"
:current-request="currentRequest"
:metric="metric"
/>
<div id="peek-view-gc" class="view"> <div id="peek-view-gc" class="view">
<span v-if="currentRequest.details" class="bold"> <span v-if="currentRequest.details" class="bold">
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span <span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span
......
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
computed: {
duration() {
return (
this.currentRequest.details[this.metric] &&
this.currentRequest.details[this.metric].duration
);
},
calls() {
return (
this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
);
},
},
};
</script>
<template>
<div :id="`peek-view-${metric}`" class="view">
<span v-if="currentRequest.details" class="bold"> {{ duration }} / {{ calls }} </span>
{{ metric }}
</div>
</template>
import $ from 'jquery'; import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { slugifyWithHyphens } from '../lib/utils/text_utility'; import { slugify } from '../lib/utils/text_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
...@@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => { ...@@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => {
}; };
const onProjectNameChange = ($projectNameInput, $projectPathInput) => { const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
const slug = slugifyWithHyphens($projectNameInput.val()); const slug = slugify($projectNameInput.val());
$projectPathInput.val(slug); $projectPathInput.val(slug);
}; };
......
...@@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex'; ...@@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import store from '../stores'; import store from '../stores';
import CollapsibleContainer from './collapsible_container.vue'; import CollapsibleContainer from './collapsible_container.vue';
import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
components: { components: {
CollapsibleContainer, CollapsibleContainer,
GlLoadingIcon, GlLoadingIcon,
SvgMessage,
}, },
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
}, },
characterError: {
type: Boolean,
required: false,
default: false,
},
helpPagePath: {
type: String,
required: true,
},
noContainersImage: {
type: String,
required: true,
},
containersErrorImage: {
type: String,
required: true,
},
repositoryUrl: {
type: String,
required: true,
},
}, },
store, store,
computed: { computed: {
...mapGetters(['isLoading', 'repos']), ...mapGetters(['isLoading', 'repos']),
dockerConnectionErrorText() {
return sprintf(
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path. For more information, please review the
%{docLinkStart}Container Registry documentation%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`,
docLinkEnd: '</a>',
},
false,
);
},
introText() {
return sprintf(
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images. Learn more about the
%{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
noContainerImagesText() {
return sprintf(
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
}, },
created() { created() {
this.setMainEndpoint(this.endpoint); this.setMainEndpoint(this.endpoint);
...@@ -33,20 +92,44 @@ export default { ...@@ -33,20 +92,44 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<gl-loading-icon v-if="isLoading" size="md" /> <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
<h4>
{{ s__('ContainerRegistry|Docker connection error') }}
</h4>
<p v-html="dockerConnectionErrorText"></p>
</svg-message>
<gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
<div v-else-if="!isLoading && !characterError && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
<svg-message
v-else-if="!isLoading && !characterError && !repos.length"
id="no-container-images"
:svg-path="noContainersImage"
>
<h4>
{{ s__('ContainerRegistry|There are no container images stored for this project') }}
</h4>
<p v-html="noContainerImagesText"></p>
<collapsible-container <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
v-for="item in repos" <p>
v-else-if="!isLoading && repos.length" {{
:key="item.id" s__(
:repo="item" 'ContainerRegistry|You can add an image to this registry with the following commands:',
/> )
}}
</p>
<p v-else-if="!isLoading && !repos.length"> <pre>
{{ docker build -t {{ repositoryUrl }} .
__(`No container images stored for this project. docker push {{ repositoryUrl }}
Add one by following the instructions above.`) </pre>
}} </svg-message>
</p>
</div> </div>
</template> </template>
<script>
export default {
name: 'RegistrySvgMessage',
props: {
id: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div :id="id" class="empty-state container-message mw-70p">
<div class="svg-content">
<img :src="svgPath" class="flex-align-self-center" />
</div>
<slot></slot>
</div>
</template>
...@@ -14,12 +14,22 @@ export default () => ...@@ -14,12 +14,22 @@ export default () =>
const { dataset } = document.querySelector(this.$options.el); const { dataset } = document.querySelector(this.$options.el);
return { return {
endpoint: dataset.endpoint, endpoint: dataset.endpoint,
characterError: Boolean(dataset.characterError),
helpPagePath: dataset.helpPagePath,
noContainersImage: dataset.noContainersImage,
containersErrorImage: dataset.containersErrorImage,
repositoryUrl: dataset.repositoryUrl,
}; };
}, },
render(createElement) { render(createElement) {
return createElement('registry-app', { return createElement('registry-app', {
props: { props: {
endpoint: this.endpoint, endpoint: this.endpoint,
characterError: this.characterError,
helpPagePath: this.helpPagePath,
noContainersImage: this.noContainersImage,
containersErrorImage: this.containersErrorImage,
repositoryUrl: this.repositoryUrl,
}, },
}); });
}, },
......
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
computed: { computed: {
releasedTimeAgo() { releasedTimeAgo() {
return sprintf(__('released %{time}'), { return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.created_at), time: this.timeFormated(this.release.released_at),
}); });
}, },
userImageAltDescription() { userImageAltDescription() {
...@@ -56,8 +56,8 @@ export default { ...@@ -56,8 +56,8 @@ export default {
<div class="card-body"> <div class="card-body">
<h2 class="card-title mt-0"> <h2 class="card-title mt-0">
{{ release.name }} {{ release.name }}
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{ <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Pre-release') __('Upcoming Release')
}}</gl-badge> }}</gl-badge>
</h2> </h2>
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
<div class="append-right-4"> <div class="append-right-4">
&bull; &bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)"> <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }} {{ releasedTimeAgo }}
</span> </span>
</div> </div>
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
</script> </script>
<template> <template>
<timeline-entry-item class="note being-posted fade-in-half"> <timeline-entry-item class="note note-wrapper being-posted fade-in-half">
<div class="timeline-icon"> <div class="timeline-icon">
<user-avatar-link <user-avatar-link
:link-href="getUserData.path" :link-href="getUserData.path"
......
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
* Container Registry * Container Registry
*/ */
.container-message {
pre {
white-space: pre-line;
}
}
.container-image { .container-image {
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
} }
......
...@@ -1093,6 +1093,17 @@ table.code { ...@@ -1093,6 +1093,17 @@ table.code {
line-height: 0; line-height: 0;
} }
.discussion-collapsible {
margin: 0 $gl-padding $gl-padding 71px;
}
.parallel {
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
}
@media (max-width: map-get($grid-breakpoints, md)-1) { @media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files { .diffs .files {
@include fixed-width-container; @include fixed-width-container;
...@@ -1110,6 +1121,11 @@ table.code { ...@@ -1110,6 +1121,11 @@ table.code {
padding-right: 0; padding-right: 0;
} }
} }
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
} }
.image-diff-overlay, .image-diff-overlay,
......
...@@ -134,6 +134,16 @@ $note-form-margin-left: 72px; ...@@ -134,6 +134,16 @@ $note-form-margin-left: 72px;
} }
} }
.discussion-toggle-replies {
border-top: 0;
border-radius: 4px 4px 0 0;
&.collapsed {
border: 0;
border-radius: 4px;
}
}
.note-created-ago, .note-created-ago,
.note-updated-at { .note-updated-at {
white-space: normal; white-space: normal;
...@@ -462,6 +472,14 @@ $note-form-margin-left: 72px; ...@@ -462,6 +472,14 @@ $note-form-margin-left: 72px;
position: relative; position: relative;
} }
.notes-content .discussion-notes.diff-discussions {
border-bottom: 1px solid $border-color;
&:nth-last-child(1) {
border-bottom: 0;
}
}
.notes_holder { .notes_holder {
font-family: $regular-font; font-family: $regular-font;
...@@ -517,6 +535,17 @@ $note-form-margin-left: 72px; ...@@ -517,6 +535,17 @@ $note-form-margin-left: 72px;
.discussion-reply-holder { .discussion-reply-holder {
border-radius: 0 0 $border-radius-default $border-radius-default; border-radius: 0 0 $border-radius-default $border-radius-default;
position: relative; position: relative;
.discussion-form {
width: 100%;
background-color: $gray-light;
padding: 0;
}
.disabled-comment {
padding: $gl-vert-padding 0;
width: 100%;
}
} }
} }
......
...@@ -46,6 +46,8 @@ module Projects ...@@ -46,6 +46,8 @@ module Projects
repository.save! if repository.has_tags? repository.save! if repository.has_tags?
end end
end end
rescue ContainerRegistry::Path::InvalidRegistryPathError
@character_error = true
end end
end end
end end
......
...@@ -99,7 +99,7 @@ module Projects ...@@ -99,7 +99,7 @@ module Projects
end end
def deploy_token_params def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry) params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end end
end end
end end
......
...@@ -35,11 +35,8 @@ module Clusters ...@@ -35,11 +35,8 @@ module Clusters
'stable/nginx-ingress' 'stable/nginx-ingress'
end end
# We will implement this in future MRs.
# Basically we need to check all dependent applications are not installed
# first.
def allowed_to_uninstall? def allowed_to_uninstall?
false external_ip_or_hostname? && application_jupyter_nil_or_installable?
end end
def install_command def install_command
...@@ -52,6 +49,10 @@ module Clusters ...@@ -52,6 +49,10 @@ module Clusters
) )
end end
def external_ip_or_hostname?
external_ip.present? || external_hostname.present?
end
def schedule_status_update def schedule_status_update
return unless installed? return unless installed?
return if external_ip return if external_ip
...@@ -63,6 +64,12 @@ module Clusters ...@@ -63,6 +64,12 @@ module Clusters
def ingress_service def ingress_service
cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE) cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
end end
private
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
end end
end end
end end
...@@ -23,9 +23,7 @@ module Clusters ...@@ -23,9 +23,7 @@ module Clusters
return unless cluster&.application_ingress_available? return unless cluster&.application_ingress_available?
ingress = cluster.application_ingress ingress = cluster.application_ingress
if ingress.external_ip || ingress.external_hostname self.status = 'installable' if ingress.external_ip_or_hostname?
self.status = 'installable'
end
end end
def chart def chart
......
...@@ -49,14 +49,6 @@ module Clusters ...@@ -49,14 +49,6 @@ module Clusters
) )
end end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files
)
end
def upgrade_command(values) def upgrade_command(values)
::Gitlab::Kubernetes::Helm::InstallCommand.new( ::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name, name: name,
......
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
# #
# - `statistic_attribute` must be an ActiveRecord attribute # - `statistic_attribute` must be an ActiveRecord attribute
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation # - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
#
module UpdateProjectStatistics module UpdateProjectStatistics
extend ActiveSupport::Concern extend ActiveSupport::Concern
include AfterCommitQueue
class_methods do class_methods do
attr_reader :project_statistics_name, :statistic_attribute attr_reader :project_statistics_name, :statistic_attribute
...@@ -31,7 +31,6 @@ module UpdateProjectStatistics ...@@ -31,7 +31,6 @@ module UpdateProjectStatistics
# #
# - project_statistics_name: A column of `ProjectStatistics` to update # - project_statistics_name: A column of `ProjectStatistics` to update
# - statistic_attribute: An attribute of the current model, default to `size` # - statistic_attribute: An attribute of the current model, default to `size`
#
def update_project_statistics(project_statistics_name:, statistic_attribute: :size) def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
@project_statistics_name = project_statistics_name @project_statistics_name = project_statistics_name
@statistic_attribute = statistic_attribute @statistic_attribute = statistic_attribute
...@@ -51,6 +50,7 @@ module UpdateProjectStatistics ...@@ -51,6 +50,7 @@ module UpdateProjectStatistics
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
update_project_statistics(delta) update_project_statistics(delta)
schedule_namespace_aggregation_worker
end end
def update_project_statistics_attribute_changed? def update_project_statistics_attribute_changed?
...@@ -59,6 +59,8 @@ module UpdateProjectStatistics ...@@ -59,6 +59,8 @@ module UpdateProjectStatistics
def update_project_statistics_after_destroy def update_project_statistics_after_destroy
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i) update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
schedule_namespace_aggregation_worker
end end
def project_destroyed? def project_destroyed?
...@@ -68,5 +70,18 @@ module UpdateProjectStatistics ...@@ -68,5 +70,18 @@ module UpdateProjectStatistics
def update_project_statistics(delta) def update_project_statistics(delta)
ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta) ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
end end
def schedule_namespace_aggregation_worker
run_after_commit do
next unless schedule_aggregation_worker?
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end
end
def schedule_aggregation_worker?
!project.nil? &&
Feature.enabled?(:update_statistics_namespace, project.root_ancestor)
end
end end
end end
...@@ -16,6 +16,14 @@ class DeployToken < ApplicationRecord ...@@ -16,6 +16,14 @@ class DeployToken < ApplicationRecord
has_many :projects, through: :project_deploy_tokens has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope validate :ensure_at_least_one_scope
validates :username,
length: { maximum: 255 },
allow_nil: true,
format: {
with: /\A[a-zA-Z0-9\.\+_-]+\z/,
message: "can contain only letters, digits, '_', '-', '+', and '.'"
}
before_save :ensure_token before_save :ensure_token
accepts_nested_attributes_for :project_deploy_tokens accepts_nested_attributes_for :project_deploy_tokens
...@@ -39,7 +47,7 @@ class DeployToken < ApplicationRecord ...@@ -39,7 +47,7 @@ class DeployToken < ApplicationRecord
end end
def username def username
"gitlab+deploy-token-#{id}" super || default_username
end end
def has_access_to?(requested_project) def has_access_to?(requested_project)
...@@ -75,4 +83,8 @@ class DeployToken < ApplicationRecord ...@@ -75,4 +83,8 @@ class DeployToken < ApplicationRecord
def ensure_at_least_one_scope def ensure_at_least_one_scope
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
end end
def default_username
"gitlab+deploy-token-#{id}" if persisted?
end
end end
...@@ -293,6 +293,10 @@ class Namespace < ApplicationRecord ...@@ -293,6 +293,10 @@ class Namespace < ApplicationRecord
end end
end end
def aggregation_scheduled?
aggregation_schedule.present?
end
private private
def parent_changed? def parent_changed?
......
# frozen_string_literal: true # frozen_string_literal: true
class Namespace::AggregationSchedule < ApplicationRecord class Namespace::AggregationSchedule < ApplicationRecord
include AfterCommitQueue
include ExclusiveLeaseGuard
self.primary_key = :namespace_id self.primary_key = :namespace_id
DEFAULT_LEASE_TIMEOUT = 3.hours
REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze
belongs_to :namespace belongs_to :namespace
after_create :schedule_root_storage_statistics
def self.delay_timeout
redis_timeout = Gitlab::Redis::SharedState.with do |redis|
redis.get(REDIS_SHARED_KEY)
end
redis_timeout.nil? ? DEFAULT_LEASE_TIMEOUT : redis_timeout.to_i
end
def schedule_root_storage_statistics
run_after_commit_or_now do
try_obtain_lease do
Namespaces::RootStatisticsWorker
.perform_async(namespace_id)
Namespaces::RootStatisticsWorker
.perform_in(self.class.delay_timeout, namespace_id)
end
end
end
private
# Used by ExclusiveLeaseGuard
def lease_timeout
self.class.delay_timeout
end
# Used by ExclusiveLeaseGuard
def lease_key
"namespace:namespaces_root_statistics:#{namespace_id}"
end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Namespace::RootStorageStatistics < ApplicationRecord class Namespace::RootStorageStatistics < ApplicationRecord
STATISTICS_ATTRIBUTES = %w(storage_size repository_size wiki_size lfs_objects_size build_artifacts_size packages_size).freeze
self.primary_key = :namespace_id self.primary_key = :namespace_id
belongs_to :namespace belongs_to :namespace
has_one :route, through: :namespace has_one :route, through: :namespace
delegate :all_projects, to: :namespace delegate :all_projects, to: :namespace
def recalculate!
update!(attributes_from_project_statistics)
end
private
def attributes_from_project_statistics
from_project_statistics
.take
.attributes
.slice(*STATISTICS_ATTRIBUTES)
end
def from_project_statistics
all_projects
.joins('INNER JOIN project_statistics ps ON ps.project_id = projects.id')
.select(
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
)
end
end end
...@@ -3,22 +3,14 @@ ...@@ -3,22 +3,14 @@
class BugzillaService < IssueTrackerService class BugzillaService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url prop_accessor :project_url, :issues_url, :new_issue_url
def title def default_title
if self.properties && self.properties['title'].present? 'Bugzilla'
self.properties['title']
else
'Bugzilla'
end
end end
def description def default_description
if self.properties && self.properties['description'].present? s_('IssueTracker|Bugzilla issue tracker')
self.properties['description']
else
'Bugzilla issue tracker'
end
end end
def self.to_param def self.to_param
......
...@@ -5,24 +5,12 @@ class CustomIssueTrackerService < IssueTrackerService ...@@ -5,24 +5,12 @@ class CustomIssueTrackerService < IssueTrackerService
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title def default_title
if self.properties && self.properties['title'].present? 'Custom Issue Tracker'
self.properties['title']
else
'Custom Issue Tracker'
end
end end
def title=(value) def default_description
self.properties['title'] = value if self.properties s_('IssueTracker|Custom issue tracker')
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
'Custom issue tracker'
end
end end
def self.to_param def self.to_param
......
...@@ -5,10 +5,18 @@ class GitlabIssueTrackerService < IssueTrackerService ...@@ -5,10 +5,18 @@ class GitlabIssueTrackerService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url prop_accessor :project_url, :issues_url, :new_issue_url
default_value_for :default, true default_value_for :default, true
def default_title
'GitLab'
end
def default_description
s_('IssueTracker|GitLab issue tracker')
end
def self.to_param def self.to_param
'gitlab' 'gitlab'
end end
......
...@@ -5,6 +5,8 @@ class IssueTrackerService < Service ...@@ -5,6 +5,8 @@ class IssueTrackerService < Service
default_value_for :category, 'issue_tracker' default_value_for :category, 'issue_tracker'
before_save :handle_properties
# Pattern used to extract links from comments # Pattern used to extract links from comments
# Override this method on services that uses different patterns # Override this method on services that uses different patterns
# This pattern does not support cross-project references # This pattern does not support cross-project references
...@@ -18,6 +20,37 @@ class IssueTrackerService < Service ...@@ -18,6 +20,37 @@ class IssueTrackerService < Service
end end
end end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
def title
if title_attribute = read_attribute(:title)
title_attribute
elsif self.properties && self.properties['title'].present?
self.properties['title']
else
default_title
end
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
def description
if description_attribute = read_attribute(:description)
description_attribute
elsif self.properties && self.properties['description'].present?
self.properties['description']
else
default_description
end
end
def handle_properties
properties.slice('title', 'description').each do |key, _|
current_value = self.properties.delete(key)
value = attribute_changed?(key) ? attribute_change(key).last : current_value
write_attribute(key, value)
end
end
def default? def default?
default default
end end
......
...@@ -17,7 +17,7 @@ class JiraService < IssueTrackerService ...@@ -17,7 +17,7 @@ class JiraService < IssueTrackerService
# Jira Cloud version is deprecating authentication via username and password. # Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud, # We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936. # for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id
before_update :reset_password before_update :reset_password
...@@ -37,7 +37,6 @@ class JiraService < IssueTrackerService ...@@ -37,7 +37,6 @@ class JiraService < IssueTrackerService
def initialize_properties def initialize_properties
super do super do
self.properties = { self.properties = {
title: issues_tracker['title'],
url: issues_tracker['url'], url: issues_tracker['url'],
api_url: issues_tracker['api_url'] api_url: issues_tracker['api_url']
} }
...@@ -74,20 +73,12 @@ class JiraService < IssueTrackerService ...@@ -74,20 +73,12 @@ class JiraService < IssueTrackerService
[Jira service documentation](#{help_page_url('user/project/integrations/jira')})." [Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
end end
def title def default_title
if self.properties && self.properties['title'].present? 'Jira'
self.properties['title']
else
'Jira'
end
end end
def description def default_description
if self.properties && self.properties['description'].present? s_('JiraService|Jira issue tracker')
self.properties['description']
else
s_('JiraService|Jira issue tracker')
end
end end
def self.to_param def self.to_param
......
...@@ -3,22 +3,14 @@ ...@@ -3,22 +3,14 @@
class RedmineService < IssueTrackerService class RedmineService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url prop_accessor :project_url, :issues_url, :new_issue_url
def title def default_title
if self.properties && self.properties['title'].present? 'Redmine'
self.properties['title']
else
'Redmine'
end
end end
def description def default_description
if self.properties && self.properties['description'].present? s_('IssueTracker|Redmine issue tracker')
self.properties['description']
else
'Redmine issue tracker'
end
end end
def self.to_param def self.to_param
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class YoutrackService < IssueTrackerService class YoutrackService < IssueTrackerService
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
prop_accessor :description, :project_url, :issues_url prop_accessor :project_url, :issues_url
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
def self.reference_pattern(only_long: false) def self.reference_pattern(only_long: false)
...@@ -14,16 +14,12 @@ class YoutrackService < IssueTrackerService ...@@ -14,16 +14,12 @@ class YoutrackService < IssueTrackerService
end end
end end
def title def default_title
'YouTrack' 'YouTrack'
end end
def description def default_description
if self.properties && self.properties['description'].present? s_('IssueTracker|YouTrack issue tracker')
self.properties['description']
else
'YouTrack issue tracker'
end
end end
def self.to_param def self.to_param
......
...@@ -12,12 +12,16 @@ class Release < ApplicationRecord ...@@ -12,12 +12,16 @@ class Release < ApplicationRecord
has_many :links, class_name: 'Releases::Link' has_many :links, class_name: 'Releases::Link'
default_value_for :released_at, allows_nil: false do
Time.zone.now
end
accepts_nested_attributes_for :links, allow_destroy: true accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) } scope :sorted, -> { order(released_at: :desc) }
delegate :repository, to: :project delegate :repository, to: :project
...@@ -44,6 +48,10 @@ class Release < ApplicationRecord ...@@ -44,6 +48,10 @@ class Release < ApplicationRecord
end end
end end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
end
private private
def actual_sha def actual_sha
......
...@@ -129,7 +129,7 @@ class Service < ApplicationRecord ...@@ -129,7 +129,7 @@ class Service < ApplicationRecord
def api_field_names def api_field_names
fields.map { |field| field[:name] } fields.map { |field| field[:name] }
.reject { |field_name| field_name =~ /(password|token|key)/ } .reject { |field_name| field_name =~ /(password|token|key|title|description)/ }
end end
def global_fields def global_fields
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
module DeployTokens module DeployTokens
class CreateService < BaseService class CreateService < BaseService
def execute def execute
@project.deploy_tokens.create(params) @project.deploy_tokens.create(params) do |deploy_token|
deploy_token.username = params[:username].presence
end
end end
end end
end end
# frozen_string_literal: true
module Namespaces
class StatisticsRefresherService
RefresherError = Class.new(StandardError)
def execute(root_namespace)
root_storage_statistics = find_or_create_root_storage_statistics(root_namespace.id)
root_storage_statistics.recalculate!
rescue ActiveRecord::ActiveRecordError => e
raise RefresherError.new(e.message)
end
private
def find_or_create_root_storage_statistics(root_namespace_id)
Namespace::RootStorageStatistics
.safe_find_or_create_by!(namespace_id: root_namespace_id)
end
end
end
...@@ -22,6 +22,10 @@ module Releases ...@@ -22,6 +22,10 @@ module Releases
params[:description] params[:description]
end end
def released_at
params[:released_at]
end
def release def release
strong_memoize(:release) do strong_memoize(:release) do
project.releases.find_by_tag(tag_name) project.releases.find_by_tag(tag_name)
......
...@@ -58,6 +58,7 @@ module Releases ...@@ -58,6 +58,7 @@ module Releases
author: current_user, author: current_user,
tag: tag.name, tag: tag.name,
sha: tag.dereferenced_target.sha, sha: tag.dereferenced_target.sha,
released_at: released_at,
links_attributes: params.dig(:assets, 'links') || [] links_attributes: params.dig(:assets, 'links') || []
) )
end end
......
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'cluster_integration_form' } do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group .form-group
%h5= s_('ClusterIntegration|Integration status') %h5= s_('ClusterIntegration|Integration status')
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= f.text_field :id, class: 'form-control w-auto', readonly: true = f.text_field :id, class: 'form-control w-auto', readonly: true
.row.prepend-top-8 .row.prepend-top-8
.form-group.col-md-9.append-bottom-0 .form-group.col-md-9
= f.label :description, _('Group description (optional)'), class: 'label-bold' = f.label :description, _('Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
......
...@@ -12,6 +12,11 @@ ...@@ -12,6 +12,11 @@
= f.label :expires_at, class: 'label-bold' = f.label :expires_at, class: 'label-bold'
= f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at = f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at
.form-group
= f.label :username, class: 'label-bold'
= f.text_field :username, class: 'form-control qa-deploy-token-username'
.text-secondary= s_('DeployTokens|Default format is "gitlab+deploy-token-{n}". Enter custom username if you want to change it.')
.form-group .form-group
= f.label :scopes, class: 'label-bold' = f.label :scopes, class: 'label-bold'
%fieldset.form-group.form-check %fieldset.form-group.form-check
......
- page_title "Container Registry"
%section %section
.settings-header
%h4
= page_title
%p
= s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
= s_('ContainerRegistry|Learn more about')
= link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
.row.registry-placeholder.prepend-bottom-10 .row.registry-placeholder.prepend-bottom-10
.col-lg-12 .col-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
"help_page_path" => help_page_path('user/project/container_registry'),
.row.prepend-top-10 "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
.col-lg-12 "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
.card "repository_url" => escape_once(@project.container_registry_url),
.card-header character_error: @character_error.to_s } }
= s_('ContainerRegistry|How to use the Container Registry')
.card-body
%p
- link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
- link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
= s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
- deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
%pre
:plain
docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}
%hr
%h5.prepend-top-default
= s_('ContainerRegistry|Use different image names')
%p.light
= s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
%pre
:plain
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
- cronjob:issue_due_scheduler - cronjob:issue_due_scheduler
- cronjob:prune_web_hook_logs - cronjob:prune_web_hook_logs
- cronjob:schedule_migrate_external_diffs - cronjob:schedule_migrate_external_diffs
- cronjob:namespaces_prune_aggregation_schedules
- gcp_cluster:cluster_install_app - gcp_cluster:cluster_install_app
- gcp_cluster:cluster_patch_app - gcp_cluster:cluster_patch_app
...@@ -101,6 +102,9 @@ ...@@ -101,6 +102,9 @@
- todos_destroyer:todos_destroyer_project_private - todos_destroyer:todos_destroyer_project_private
- todos_destroyer:todos_destroyer_private_features - todos_destroyer:todos_destroyer_private_features
- update_namespace_statistics:namespaces_schedule_aggregation
- update_namespace_statistics:namespaces_root_statistics
- object_pool:object_pool_create - object_pool:object_pool_create
- object_pool:object_pool_schedule_join - object_pool:object_pool_schedule_join
- object_pool:object_pool_join - object_pool:object_pool_join
......
# frozen_string_literal: true
module Namespaces
class PruneAggregationSchedulesWorker
include ApplicationWorker
include CronjobQueue
# Worker to prune pending rows on Namespace::AggregationSchedule
# It's scheduled to run once a day at 1:05am.
def perform
aggregation_schedules.find_each do |aggregation_schedule|
aggregation_schedule.schedule_root_storage_statistics
end
end
private
def aggregation_schedules
Namespace::AggregationSchedule.all
end
end
end
# frozen_string_literal: true
module Namespaces
class RootStatisticsWorker
include ApplicationWorker
queue_namespace :update_namespace_statistics
def perform(namespace_id)
namespace = Namespace.find(namespace_id)
return unless update_statistics_enabled_for?(namespace) && namespace.aggregation_scheduled?
Namespaces::StatisticsRefresherService.new.execute(namespace)
namespace.aggregation_schedule.destroy
rescue ::Namespaces::StatisticsRefresherService::RefresherError, ActiveRecord::RecordNotFound => ex
log_error(namespace.full_path, ex.message) if namespace
end
private
def log_error(namespace_path, error_message)
Gitlab::SidekiqLogger.error("Namespace statistics can't be updated for #{namespace_path}: #{error_message}")
end
def update_statistics_enabled_for?(namespace)
Feature.enabled?(:update_statistics_namespace, namespace)
end
end
end
# frozen_string_literal: true
module Namespaces
class ScheduleAggregationWorker
include ApplicationWorker
queue_namespace :update_namespace_statistics
def perform(namespace_id)
return unless aggregation_schedules_table_exists?
namespace = Namespace.find(namespace_id)
root_ancestor = namespace.root_ancestor
return unless update_statistics_enabled_for?(root_ancestor) && !root_ancestor.aggregation_scheduled?
Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: root_ancestor.id)
rescue ActiveRecord::RecordNotFound
log_error(namespace_id)
end
private
# On db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb
# traces are archived through build.trace.archive, which in consequence
# calls UpdateProjectStatistics#schedule_namespace_statistics_worker.
#
# The migration and specs fails since NamespaceAggregationSchedule table
# does not exist at that point.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/50712
def aggregation_schedules_table_exists?
return true unless Rails.env.test?
Namespace::AggregationSchedule.table_exists?
end
def log_error(root_ancestor_id)
Gitlab::SidekiqLogger.error("Namespace can't be scheduled for aggregation: #{root_ancestor_id} does not exist")
end
def update_statistics_enabled_for?(root_ancestor)
Feature.enabled?(:update_statistics_namespace, root_ancestor)
end
end
end
---
title: Resolve Multiple discussions per line in merge request diffs
merge_request: 28748
author:
type: added
---
title: Updated container registry to display error message when special characters in path. Documentation has also been updated.
merge_request: 29616
author:
type: changed
---
title: Allow custom username for deploy tokens
merge_request: 29639
author:
type: added
---
title: Implement borderless discussion design with new reply field
merge_request: 28580
author:
type: added
---
title: Remove istanbul JavaScript package
merge_request: 30232
author: Takuya Noguchi
type: other
---
title: Allow Ingress to be uninstalled from the UI
merge_request: 29977
author:
type: added
---
title: Show an Upcoming Status for Releases
merge_request: 29577
author:
type: added
---
title: Don't let logged out user do manual order
merge_request: 30264
author:
type: fixed
---
title: Cache Flipper persisted names directly to local memory storage
merge_request: 30265
author:
type: performance
---
title: Add Redis call details in Peek performance bar
merge_request: 30191
author:
type: changed
---
title: Replace slugifyWithHyphens with improved slugify function
merge_request: 30172
author: Luke Ward
type: fixed
---
title: Make sure UnicornSampler is started only in master process.
merge_request: 30215
author:
type: fixed
---
title: Use PostgreSQL 9.6.11 in CI tests
merge_request: 30270
author: Takuya Noguchi
type: other
---
title: Fix typo in updateResolvableDiscussionsCounts action
merge_request: 30278
author: Frank van Rest
type: other
...@@ -441,6 +441,9 @@ Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLog ...@@ -441,6 +441,9 @@ Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLog
Settings.cron_jobs['schedule_migrate_external_diffs_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['schedule_migrate_external_diffs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *' Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker' Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker'
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['cron'] ||= '5 1 * * *'
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['job_class'] = 'Namespaces::PruneAggregationSchedulesWorker'
Gitlab.ee do Gitlab.ee do
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
......
require 'prometheus/client' require 'prometheus/client'
require 'prometheus/client/support/unicorn' require 'prometheus/client/support/unicorn'
# Keep separate directories for separate processes
def prometheus_default_multiproc_dir
return unless Rails.env.development? || Rails.env.test?
if Sidekiq.server?
Rails.root.join('tmp/prometheus_multiproc_dir/sidekiq')
elsif defined?(Unicorn::Worker)
Rails.root.join('tmp/prometheus_multiproc_dir/unicorn')
elsif defined?(::Puma)
Rails.root.join('tmp/prometheus_multiproc_dir/puma')
else
Rails.root.join('tmp/prometheus_multiproc_dir')
end
end
Prometheus::Client.configure do |config| Prometheus::Client.configure do |config|
config.logger = Rails.logger config.logger = Rails.logger
config.initial_mmap_file_size = 4 * 1024 config.initial_mmap_file_size = 4 * 1024
config.multiprocess_files_dir = ENV['prometheus_multiproc_dir']
if Rails.env.development? || Rails.env.test? config.multiprocess_files_dir = ENV['prometheus_multiproc_dir'] || prometheus_default_multiproc_dir
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider) config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
end end
...@@ -29,15 +41,13 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? ...@@ -29,15 +41,13 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Cluster::LifecycleEvents.on_worker_start do Gitlab::Cluster::LifecycleEvents.on_worker_start do
defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
if defined?(::Unicorn)
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
end
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
end end
if defined?(::Puma) Gitlab::Cluster::LifecycleEvents.on_master_start do
Gitlab::Cluster::LifecycleEvents.on_master_start do if defined?(::Unicorn)
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
elsif defined?(::Puma)
Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start
end end
end end
......
...@@ -4,12 +4,22 @@ ...@@ -4,12 +4,22 @@
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req| ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
if [:throttle, :blacklist].include? req.env['rack.attack.match_type'] if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
Gitlab::AuthLogger.error( rack_attack_info = {
message: 'Rack_Attack', message: 'Rack_Attack',
env: req.env['rack.attack.match_type'], env: req.env['rack.attack.match_type'],
ip: req.ip, ip: req.ip,
request_method: req.request_method, request_method: req.request_method,
fullpath: req.fullpath fullpath: req.fullpath
) }
if req.env['rack.attack.matched'] != 'throttle_unauthenticated'
user_id = req.env['rack.attack.match_discriminator']
user = User.find_by(id: user_id)
rack_attack_info[:user_id] = user_id
rack_attack_info[:username] = user.username unless user.nil?
end
Gitlab::AuthLogger.error(rack_attack_info)
end end
end end
...@@ -94,6 +94,7 @@ ...@@ -94,6 +94,7 @@
- [migrate_external_diffs, 1] - [migrate_external_diffs, 1]
- [update_project_statistics, 1] - [update_project_statistics, 1]
- [phabricator_import_import_tasks, 1] - [phabricator_import_import_tasks, 1]
- [update_namespace_statistics, 1]
# EE-specific queues # EE-specific queues
- [ldap_group_sync, 2] - [ldap_group_sync, 2]
......
This diff is collapsed.
class FixNamespaces < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
namespaces = exec_query('SELECT id, path FROM namespaces WHERE name <> path and type is null')
namespaces.each do |row|
id = row['id']
path = row['path']
exec_query("UPDATE namespaces SET name = '#{path}' WHERE id = #{id}")
end
end
def down
end
end
class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration[4.2]
def up
change_column :merge_request_diffs, :state, :string, null: true,
default: nil
end
def down
change_column :merge_request_diffs, :state, :string, null: false,
default: 'collected'
end
end
# rubocop:disable all
class AddIndexOnIid < ActiveRecord::Migration[4.2]
def change
RemoveDuplicateIid.clean(Issue)
RemoveDuplicateIid.clean(MergeRequest, 'target_project_id')
RemoveDuplicateIid.clean(Milestone)
add_index :issues, [:project_id, :iid], unique: true
add_index :merge_requests, [:target_project_id, :iid], unique: true
add_index :milestones, [:project_id, :iid], unique: true
end
end
class RemoveDuplicateIid
def self.clean(klass, project_field = 'project_id')
duplicates = klass.find_by_sql("SELECT iid, #{project_field} FROM #{klass.table_name} GROUP BY #{project_field}, iid HAVING COUNT(*) > 1")
duplicates.each do |duplicate|
project_id = duplicate.send(project_field)
iid = duplicate.iid
items = klass.of_projects(project_id).where(iid: iid)
if items.size > 1
puts "Remove #{klass.name} duplicates for iid: #{iid} and project_id: #{project_id}"
items.shift
items.each do |item|
item.destroy
puts '.'
end
end
end
end
end
# rubocop:disable all
class IndexOnCurrentSignInAt < ActiveRecord::Migration[4.2]
def change
add_index :users, :current_sign_in_at
end
end
# rubocop:disable all
class AddNotesIndexUpdatedAt < ActiveRecord::Migration[4.2]
def change
add_index :notes, :updated_at
end
end
# rubocop:disable all
class AddRepoSizeToDb < ActiveRecord::Migration[4.2]
def change
add_column :projects, :repository_size, :float, default: 0
end
end
# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
path = File.join(namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new('default', path, '', '')
if repo.empty?
print '-'
else
size = repo.size
print '.'
execute("UPDATE projects SET repository_size = #{size} WHERE id = #{id}")
end
rescue => e
puts "\nFailed to update project #{id}: #{e}"
end
end
puts "\nDone"
end
def down
end
end
# rubocop:disable all
class AddPositionToMergeRequest < ActiveRecord::Migration[4.2]
def change
add_column :merge_requests, :position, :integer, default: 0
end
end
# rubocop:disable all
class CreateUsersStarProjects < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
create_table :users_star_projects do |t|
t.integer :project_id, null: false
t.integer :user_id, null: false
t.timestamps null: true
end
add_index :users_star_projects, :user_id
add_index :users_star_projects, :project_id
add_index :users_star_projects, [:user_id, :project_id], unique: true
add_column :projects, :star_count, :integer, default: 0, null: false
add_index :projects, :star_count, using: :btree
end
end
# rubocop:disable all
class CreateLabels < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
create_table :labels do |t|
t.string :title
t.string :color
t.integer :project_id
t.timestamps null: true
end
end
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.
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.
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.
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