Commit 026cc147 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into stateful_deployments-ee

parents c5dd5ce2 8c31b2f4
...@@ -167,7 +167,7 @@ export default { ...@@ -167,7 +167,7 @@ export default {
<button <button
v-if="shouldShowCommentButton" v-if="shouldShowCommentButton"
type="button" type="button"
class="add-diff-note js-add-diff-note-button" class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line" title="Add a comment to this line"
@click="handleCommentButton" @click="handleCommentButton"
> >
......
...@@ -102,7 +102,7 @@ export default { ...@@ -102,7 +102,7 @@ export default {
:line-type="newLineType" :line-type="newLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
class="diff-line-num new_line" class="diff-line-num new_line qa-new-diff-line"
/> />
<td <td
:class="line.type" :class="line.type"
......
<script> <script>
import { s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -28,10 +30,24 @@ export default { ...@@ -28,10 +30,24 @@ export default {
}, },
}, },
methods: { methods: {
onClickAction(endpoint) { onClickAction(action) {
if (action.scheduledAt) {
const confirmationMessage = sprintf(
s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
),
{ jobName: action.name },
);
// https://gitlab.com/gitlab-org/gitlab-ce/issues/52156
// eslint-disable-next-line no-alert
if (!window.confirm(confirmationMessage)) {
return;
}
}
this.isLoading = true; this.isLoading = true;
eventHub.$emit('postAction', { endpoint }); eventHub.$emit('postAction', { endpoint: action.playPath });
}, },
isActionDisabled(action) { isActionDisabled(action) {
...@@ -41,6 +57,11 @@ export default { ...@@ -41,6 +57,11 @@ export default {
return !action.playable; return !action.playable;
}, },
remainingTime(action) {
const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now();
return formatTime(Math.max(0, remainingMilliseconds));
},
}, },
}; };
</script> </script>
...@@ -54,7 +75,7 @@ export default { ...@@ -54,7 +75,7 @@ export default {
:aria-label="title" :aria-label="title"
:disabled="isLoading" :disabled="isLoading"
type="button" type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
data-container="body" data-container="body"
data-toggle="dropdown" data-toggle="dropdown"
> >
...@@ -75,12 +96,19 @@ export default { ...@@ -75,12 +96,19 @@ export default {
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)" :disabled="isActionDisabled(action)"
type="button" type="button"
class="js-manual-action-link no-btn btn" class="js-manual-action-link no-btn btn d-flex align-items-center"
@click="onClickAction(action.play_path)" @click="onClickAction(action)"
> >
<span> <span class="flex-fill">
{{ action.name }} {{ action.name }}
</span> </span>
<span
v-if="action.scheduledAt"
class="text-secondary"
>
<icon name="clock" />
{{ remainingTime(action) }}
</span>
</button> </button>
</li> </li>
</ul> </ul>
......
...@@ -13,6 +13,7 @@ import TerminalButtonComponent from './environment_terminal_button.vue'; ...@@ -13,6 +13,7 @@ import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue'; import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
/** /**
* Environment Item Component * Environment Item Component
...@@ -73,21 +74,6 @@ export default { ...@@ -73,21 +74,6 @@ export default {
return false; return false;
}, },
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0
);
},
/** /**
* Checkes whether the environment is protected. * Checkes whether the environment is protected.
* (`is_protected` currently only set in EE) * (`is_protected` currently only set in EE)
...@@ -154,23 +140,20 @@ export default { ...@@ -154,23 +140,20 @@ export default {
return ''; return '';
}, },
/** actions() {
* Returns the manual actions with the name parsed. if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map(action => {
const parsedAction = {
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
}
return []; return [];
}
const { manualActions, scheduledActions } = convertObjectPropsToCamelCase(
this.model.last_deployment,
{ deep: true },
);
const combinedActions = (manualActions || []).concat(scheduledActions || []);
return combinedActions.map(action => ({
...action,
name: humanize(action.name),
}));
}, },
/** /**
...@@ -443,7 +426,7 @@ export default { ...@@ -443,7 +426,7 @@ export default {
displayEnvironmentActions() { displayEnvironmentActions() {
return ( return (
this.hasManualActions || this.actions.length > 0 ||
this.externalURL || this.externalURL ||
this.monitoringUrl || this.monitoringUrl ||
this.canStopEnvironment || this.canStopEnvironment ||
...@@ -641,8 +624,8 @@ export default { ...@@ -641,8 +624,8 @@ export default {
/> />
<actions-component <actions-component
v-if="hasManualActions && canCreateDeployment" v-if="actions.length > 0"
:actions="manualActions" :actions="actions"
/> />
<terminal-button-component <terminal-button-component
......
<script> <script>
import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
TimeagoTooltip, TimeagoTooltip,
GlLink,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
...@@ -53,16 +55,16 @@ export default { ...@@ -53,16 +55,16 @@ export default {
class="btn-group d-flex" class="btn-group d-flex"
role="group" role="group"
> >
<a <gl-link
v-if="artifact.keep_path" v-if="artifact.keep_path"
:href="artifact.keep_path" :href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default" class="js-keep-artifacts btn btn-sm btn-default"
data-method="post" data-method="post"
> >
{{ s__('Job|Keep') }} {{ s__('Job|Keep') }}
</a> </gl-link>
<a <gl-link
v-if="artifact.download_path" v-if="artifact.download_path"
:href="artifact.download_path" :href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default" class="js-download-artifacts btn btn-sm btn-default"
...@@ -70,15 +72,15 @@ export default { ...@@ -70,15 +72,15 @@ export default {
rel="nofollow" rel="nofollow"
> >
{{ s__('Job|Download') }} {{ s__('Job|Download') }}
</a> </gl-link>
<a <gl-link
v-if="artifact.browse_path" v-if="artifact.browse_path"
:href="artifact.browse_path" :href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default" class="js-browse-artifacts btn btn-sm btn-default"
> >
{{ s__('Job|Browse') }} {{ s__('Job|Browse') }}
</a> </gl-link>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { GlLink } from '@gitlab-org/gitlab-ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
GlLink,
}, },
props: { props: {
commit: { commit: {
...@@ -31,10 +33,10 @@ export default { ...@@ -31,10 +33,10 @@ export default {
<p> <p>
{{ __('Commit') }} {{ __('Commit') }}
<a <gl-link
:href="commit.commit_path" :href="commit.commit_path"
class="js-commit-sha commit-sha link-commit" class="js-commit-sha commit-sha link-commit"
>{{ commit.short_id }}</a> >{{ commit.short_id }}</gl-link>
<clipboard-button <clipboard-button
:text="commit.short_id" :text="commit.short_id"
...@@ -42,11 +44,11 @@ export default { ...@@ -42,11 +44,11 @@ export default {
css-class="btn btn-clipboard btn-transparent" css-class="btn btn-clipboard btn-transparent"
/> />
<a <gl-link
v-if="mergeRequest" v-if="mergeRequest"
:href="mergeRequest.path" :href="mergeRequest.path"
class="js-link-commit link-commit" class="js-link-commit link-commit"
>!{{ mergeRequest.iid }}</a> >!{{ mergeRequest.iid }}</gl-link>
</p> </p>
<p class="build-light-text append-bottom-0"> <p class="build-light-text append-bottom-0">
......
<script> <script>
import { GlLink } from '@gitlab-org/gitlab-ui';
export default { export default {
components: {
GlLink,
},
props: { props: {
illustrationPath: { illustrationPath: {
type: String, type: String,
...@@ -62,13 +67,13 @@ export default { ...@@ -62,13 +67,13 @@ export default {
v-if="action" v-if="action"
class="text-center" class="text-center"
> >
<a <gl-link
:href="action.path" :href="action.path"
:data-method="action.method" :data-method="action.method"
class="js-job-empty-state-action btn btn-primary" class="js-job-empty-state-action btn btn-primary"
> >
{{ action.button_title }} {{ action.button_title }}
</a> </gl-link>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
TimeagoTooltip, TimeagoTooltip,
GlLink,
}, },
props: { props: {
user: { user: {
...@@ -29,9 +31,9 @@ export default { ...@@ -29,9 +31,9 @@ export default {
<div class="erased alert alert-warning"> <div class="erased alert alert-warning">
<template v-if="isErasedByUser"> <template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }} {{ s__("Job|Job has been erased by") }}
<a :href="user.web_url"> <gl-link :href="user.web_url">
{{ user.username }} {{ user.username }}
</a> </gl-link>
</template> </template>
<template v-else> <template v-else>
{{ s__("Job|Job has been erased") }} {{ s__("Job|Job has been erased") }}
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
...@@ -26,6 +27,7 @@ export default { ...@@ -26,6 +27,7 @@ export default {
EmptyState, EmptyState,
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
GlLoadingIcon,
Log, Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
......
<script> <script>
import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: { components: {
CiIcon, CiIcon,
Icon, Icon,
GlLink,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
job: { job: {
...@@ -37,11 +38,10 @@ export default { ...@@ -37,11 +38,10 @@ export default {
active: isActive active: isActive
}" }"
> >
<a <gl-link
v-tooltip v-gl-tooltip
:href="job.status.details_path" :href="job.status.details_path"
:title="tooltipText" :title="tooltipText"
data-container="body"
data-boundary="viewport" data-boundary="viewport"
class="js-job-link" class="js-job-link"
> >
...@@ -60,6 +60,6 @@ export default { ...@@ -60,6 +60,6 @@ export default {
name="retry" name="retry"
class="js-retry-icon" class="js-retry-icon"
/> />
</a> </gl-link>
</div> </div>
</template> </template>
<script> <script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import { polyfillSticky } from '~/lib/utils/sticky'; import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg'; import scrollDown from '../svg/scroll_down.svg';
...@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg'; ...@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg';
export default { export default {
components: { components: {
Icon, Icon,
GlLink,
GlButton,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
scrollDown, scrollDown,
props: { props: {
...@@ -73,76 +75,70 @@ export default { ...@@ -73,76 +75,70 @@ export default {
<template v-if="isTraceSizeVisible"> <template v-if="isTraceSizeVisible">
{{ jobLogSize }} {{ jobLogSize }}
<a <gl-link
v-if="rawPath" v-if="rawPath"
:href="rawPath" :href="rawPath"
class="js-raw-link raw-link" class="js-raw-link raw-link"
> >
{{ s__("Job|Complete Raw") }} {{ s__("Job|Complete Raw") }}
</a> </gl-link>
</template> </template>
</div> </div>
<!-- eo truncate information --> <!-- eo truncate information -->
<div class="controllers float-right"> <div class="controllers float-right">
<!-- links --> <!-- links -->
<a <gl-link
v-if="rawPath" v-if="rawPath"
v-tooltip v-gl-tooltip.body
:title="s__('Job|Show complete raw')" :title="s__('Job|Show complete raw')"
:href="rawPath" :href="rawPath"
class="js-raw-link-controller controllers-buttons" class="js-raw-link-controller controllers-buttons"
data-container="body"
> >
<icon name="doc-text" /> <icon name="doc-text" />
</a> </gl-link>
<a <gl-link
v-if="erasePath" v-if="erasePath"
v-tooltip v-gl-tooltip.body
:title="s__('Job|Erase job log')" :title="s__('Job|Erase job log')"
:href="erasePath" :href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')" :data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons" class="js-erase-link controllers-buttons"
data-container="body"
data-method="post" data-method="post"
> >
<icon name="remove" /> <icon name="remove" />
</a> </gl-link>
<!-- eo links --> <!-- eo links -->
<!-- scroll buttons --> <!-- scroll buttons -->
<div <div
v-tooltip v-gl-tooltip
:title="s__('Job|Scroll to top')" :title="s__('Job|Scroll to top')"
class="controllers-buttons" class="controllers-buttons"
data-container="body"
> >
<button <gl-button
:disabled="isScrollTopDisabled" :disabled="isScrollTopDisabled"
type="button" type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank" class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop" @click="handleScrollToTop"
> >
<icon name="scroll_up"/> <icon name="scroll_up" />
</button> </gl-button>
</div> </div>
<div <div
v-tooltip v-gl-tooltip
:title="s__('Job|Scroll to bottom')" :title="s__('Job|Scroll to bottom')"
class="controllers-buttons" class="controllers-buttons"
data-container="body"
> >
<button <gl-button
:disabled="isScrollBottomDisabled" :disabled="isScrollBottomDisabled"
type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank" class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }" :class="{ animate: isScrollingDown }"
@click="handleScrollToBottom" @click="handleScrollToBottom"
v-html="$options.scrollDown" v-html="$options.scrollDown"
> />
</button>
</div> </div>
<!-- eo scroll buttons --> <!-- eo scroll buttons -->
</div> </div>
......
<script> <script>
import { GlLink } from '@gitlab-org/gitlab-ui';
export default { export default {
name: 'SidebarDetailRow', name: 'SidebarDetailRow',
components: {
GlLink,
},
props: { props: {
title: { title: {
type: String, type: String,
...@@ -41,7 +46,7 @@ export default { ...@@ -41,7 +46,7 @@ export default {
v-if="hasHelpURL" v-if="hasHelpURL"
class="help-button float-right" class="help-button float-right"
> >
<a <gl-link
:href="helpUrl" :href="helpUrl"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
...@@ -50,7 +55,7 @@ export default { ...@@ -50,7 +55,7 @@ export default {
class="fa fa-question-circle" class="fa fa-question-circle"
aria-hidden="true" aria-hidden="true"
></i> ></i>
</a> </gl-link>
</span> </span>
</p> </p>
</template> </template>
<script> <script>
import { GlLink } from '@gitlab-org/gitlab-ui';
/** /**
* Renders Stuck Runners block for job's view. * Renders Stuck Runners block for job's view.
*/ */
export default { export default {
components: {
GlLink,
},
props: { props: {
hasNoRunnersForProject: { hasNoRunnersForProject: {
type: Boolean, type: Boolean,
...@@ -52,12 +56,12 @@ export default { ...@@ -52,12 +56,12 @@ export default {
</p> </p>
{{ __("Go to") }} {{ __("Go to") }}
<a <gl-link
v-if="runnersPath" v-if="runnersPath"
:href="runnersPath" :href="runnersPath"
class="js-runners-path" class="js-runners-path"
> >
{{ __("Runners page") }} {{ __("Runners page") }}
</a> </gl-link>
</div> </div>
</template> </template>
...@@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled" :disabled="isSubmitButtonDisabled"
name="button" name="button"
type="button" type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static" data-display="static"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Open comment type dropdown"> aria-label="Open comment type dropdown">
...@@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button <button
type="button" type="button"
class="btn btn-transparent" class="btn btn-transparent qa-discussion-option"
@click.prevent="setNoteType('discussion')"> @click.prevent="setNoteType('discussion')">
<i <i
aria-hidden="true" aria-hidden="true"
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
export default { export default {
components: { components: {
...@@ -12,14 +13,17 @@ export default { ...@@ -12,14 +13,17 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
defaultValue: { selectedValue: {
type: Number, type: Number,
default: null, default: null,
required: false, required: false,
}, },
}, },
data() { data() {
return { currentValue: this.defaultValue }; return {
currentValue: this.selectedValue,
defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
};
}, },
computed: { computed: {
...mapGetters(['getNotesDataByProp']), ...mapGetters(['getNotesDataByProp']),
...@@ -28,8 +32,11 @@ export default { ...@@ -28,8 +32,11 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue); return this.filters.find(filter => filter.value === this.currentValue);
}, },
}, },
mounted() {
this.toggleCommentsForm();
},
methods: { methods: {
...mapActions(['filterDiscussion']), ...mapActions(['filterDiscussion', 'setCommentsDisabled']),
selectFilter(value) { selectFilter(value) {
const filter = parseInt(value, 10); const filter = parseInt(value, 10);
...@@ -39,6 +46,10 @@ export default { ...@@ -39,6 +46,10 @@ export default {
if (filter === this.currentValue) return; if (filter === this.currentValue) return;
this.currentValue = filter; this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
this.toggleCommentsForm();
},
toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
}, },
}, },
}; };
...@@ -73,6 +84,10 @@ export default { ...@@ -73,6 +84,10 @@ export default {
> >
{{ filter.title }} {{ filter.title }}
</button> </button>
<div
v-if="filter.value === defaultValue"
class="dropdown-divider"
></div>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -191,7 +191,7 @@ export default { ...@@ -191,7 +191,7 @@ export default {
:data-supports-quick-actions="!isEditing" :data-supports-quick-actions="!isEditing"
name="note[note]" name="note[note]"
class="note-textarea js-gfm-input js-note-text class="note-textarea js-gfm-input js-note-text
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description" aria-label="Description"
placeholder="Write a comment or drag your files here…" placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()" @keydown.meta.enter="handleUpdate()"
...@@ -216,6 +216,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -216,6 +216,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<input <input
v-model="isUnresolving" v-model="isUnresolving"
type="checkbox" type="checkbox"
class="qa-unresolve-review-discussion"
/> />
{{ __('Unresolve discussion') }} {{ __('Unresolve discussion') }}
</template> </template>
...@@ -225,6 +226,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -225,6 +226,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<input <input
v-model="isResolving" v-model="isResolving"
type="checkbox" type="checkbox"
class="qa-resolve-review-discussion"
/> />
{{ __('Resolve discussion') }} {{ __('Resolve discussion') }}
</template> </template>
...@@ -234,7 +236,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -234,7 +236,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<button <button
:disabled="isDisabled" :disabled="isDisabled"
type="button" type="button"
class="btn btn-success" class="btn btn-success qa-start-review"
@click="handleAddToReview()"> @click="handleAddToReview()">
<template v-if="hasDrafts"> <template v-if="hasDrafts">
{{ __('Add to review') }} {{ __('Add to review') }}
...@@ -246,7 +248,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -246,7 +248,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
<button <button
:disabled="isDisabled" :disabled="isDisabled"
type="button" type="button"
class="btn" class="btn qa-comment-now"
@click="handleUpdate()"> @click="handleUpdate()">
{{ __('Add comment now') }} {{ __('Add comment now') }}
</button> </button>
......
...@@ -385,7 +385,7 @@ Please check your network connection and try again.`; ...@@ -385,7 +385,7 @@ Please check your network connection and try again.`;
role="group"> role="group">
<button <button
type="button" type="button"
class="js-vue-discussion-reply btn btn-text-field mr-2" class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
title="Add a reply" title="Add a reply"
@click="showReplyForm">Reply...</button> @click="showReplyForm">Reply...</button>
</div> </div>
......
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
'getNotesDataByProp', 'getNotesDataByProp',
'discussionCount', 'discussionCount',
'isLoading', 'isLoading',
'commentsDisabled',
]), ]),
noteableType() { noteableType() {
return this.noteableData.noteableType; return this.noteableData.noteableType;
...@@ -206,6 +207,7 @@ export default { ...@@ -206,6 +207,7 @@ export default {
</ul> </ul>
<comment-form <comment-form
v-if="!commentsDisabled"
:noteable-type="noteableType" :noteable-type="noteableType"
:markdown-version="markdownVersion" :markdown-version="markdownVersion"
/> />
......
...@@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; ...@@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description'; export const DESCRIPTION_TYPE = 'changed the description';
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const NOTEABLE_TYPE_MAPPING = { export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE, Issue: ISSUE_NOTEABLE_TYPE,
......
...@@ -6,7 +6,7 @@ export default store => { ...@@ -6,7 +6,7 @@ export default store => {
if (discussionFilterEl) { if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset; const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
const filters = Object.keys(filterValues).map(entry => ({ const filters = Object.keys(filterValues).map(entry => ({
title: entry, title: entry,
...@@ -24,7 +24,7 @@ export default store => { ...@@ -24,7 +24,7 @@ export default store => {
return createElement('discussion-filter', { return createElement('discussion-filter', {
props: { props: {
filters, filters,
defaultValue, selectedValue,
}, },
}); });
}, },
......
...@@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => { ...@@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => {
}); });
}; };
export const setCommentsDisabled = ({ commit }, data) => {
commit(types.DISABLE_COMMENTS, data);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -195,5 +195,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { ...@@ -195,5 +195,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
export const getDiscussion = state => discussionId => export const getDiscussion = state => discussionId =>
state.discussions.find(discussion => discussion.id === discussionId); state.discussions.find(discussion => discussion.id === discussionId);
export const commentsDisabled = state => state.commentsDisabled;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -21,6 +21,7 @@ export default () => ({ ...@@ -21,6 +21,7 @@ export default () => ({
noteableData: { noteableData: {
current_user: {}, current_user: {},
}, },
commentsDisabled: false,
}, },
actions, actions,
getters, getters,
......
...@@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; ...@@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
// DISCUSSION // DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
......
...@@ -225,4 +225,8 @@ export default { ...@@ -225,4 +225,8 @@ export default {
discussion.truncated_diff_lines = diffLines; discussion.truncated_diff_lines = diffLines;
}, },
[types.DISABLE_COMMENTS](state, value) {
state.commentsDisabled = value;
},
}; };
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
if (action.scheduled_at) { if (action.scheduled_at) {
const confirmationMessage = sprintf( const confirmationMessage = sprintf(
s__( s__(
"DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.", "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
), ),
{ jobName: action.name }, { jobName: action.name },
); );
......
...@@ -322,15 +322,15 @@ ...@@ -322,15 +322,15 @@
width: $contextual-sidebar-width - 1px; width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
position: fixed; position: fixed;
height: $toggle-sidebar-height;
bottom: 0; bottom: 0;
padding: $gl-padding; padding: 0 $gl-padding;
background-color: $gray-light; background-color: $gray-light;
border: 0; border: 0;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 1;
svg { svg {
margin-right: 8px; margin-right: 8px;
......
...@@ -291,7 +291,7 @@ ...@@ -291,7 +291,7 @@
/* /*
* Mixin that handles the position of the controls placed on the top bar * Mixin that handles the position of the controls placed on the top bar
*/ */
@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { @mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
display: flex; display: flex;
font-size: $control-font-size; font-size: $control-font-size;
justify-content: $flex-direction; justify-content: $flex-direction;
...@@ -304,8 +304,9 @@ ...@@ -304,8 +304,9 @@
svg { svg {
width: 15px; width: 15px;
height: 15px; height: 15px;
display: block; display: $svg-display;
fill: $gl-text-color; fill: $gl-text-color;
top: $svg-top;
} }
.controllers-buttons { .controllers-buttons {
......
...@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px; ...@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s; $default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px; $contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px; $contextual-sidebar-collapsed-width: 50px;
$toggle-sidebar-height: 48px;
/* /*
* Color schema * Color schema
......
...@@ -57,10 +57,6 @@ ...@@ -57,10 +57,6 @@
.top-bar { .top-bar {
@include build-trace-top-bar(35px); @include build-trace-top-bar(35px);
&.sidebar-expanded {
margin-right: calc(#{$gutter-width} - 16px);
}
.truncated-info { .truncated-info {
.truncated-info-size { .truncated-info-size {
margin: 0 5px; margin: 0 5px;
...@@ -74,7 +70,7 @@ ...@@ -74,7 +70,7 @@
} }
.controllers { .controllers {
@include build-controllers(15px, center, false, 0); @include build-controllers(15px, center, false, 0, inline, 0);
} }
} }
......
...@@ -44,11 +44,6 @@ ...@@ -44,11 +44,6 @@
margin: 0; margin: 0;
} }
.icon-play {
height: 13px;
width: 12px;
}
.external-url, .external-url,
.dropdown-new { .dropdown-new {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
...@@ -526,7 +521,7 @@ ...@@ -526,7 +521,7 @@
} }
.arrow-shadow { .arrow-shadow {
content: ""; content: '';
position: absolute; position: absolute;
width: 7px; width: 7px;
height: 7px; height: 7px;
......
...@@ -116,6 +116,7 @@ module ApplicationSettingsHelper ...@@ -116,6 +116,7 @@ module ApplicationSettingsHelper
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:allow_local_requests_from_hooks_and_services, :allow_local_requests_from_hooks_and_services,
:archive_builds_in_human_readable,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
......
...@@ -109,6 +109,8 @@ module IconsHelper ...@@ -109,6 +109,8 @@ module IconsHelper
def file_type_icon_class(type, mode, name) def file_type_icon_class(type, mode, name)
if type == 'folder' if type == 'folder'
icon_class = 'folder' icon_class = 'folder'
elsif type == 'archive'
icon_class = 'archive'
elsif mode == '120000' elsif mode == '120000'
icon_class = 'share' icon_class = 'share'
else else
......
...@@ -31,11 +31,21 @@ module TreeHelper ...@@ -31,11 +31,21 @@ module TreeHelper
# mode - File unix mode # mode - File unix mode
# name - File name # name - File name
def tree_icon(type, mode, name) def tree_icon(type, mode, name)
icon("#{file_type_icon_class(type, mode, name)} fw") icon([file_type_icon_class(type, mode, name), 'fw'])
end end
def tree_hex_class(content) # Using Rails `*_path` methods can be slow, especially when generating
"file_#{hexdigest(content.name)}" # many paths, as with a repository tree that has thousands of items.
def fast_project_blob_path(project, blob_path)
Addressable::URI.escape(
File.join(relative_url_root, project.path_with_namespace, 'blob', blob_path)
)
end
def fast_project_tree_path(project, tree_path)
Addressable::URI.escape(
File.join(relative_url_root, project.path_with_namespace, 'tree', tree_path)
)
end end
# Simple shortcut to File.join # Simple shortcut to File.join
...@@ -142,4 +152,8 @@ module TreeHelper ...@@ -142,4 +152,8 @@ module TreeHelper
def selected_branch def selected_branch
@branch_name || tree_edit_branch @branch_name || tree_edit_branch
end end
def relative_url_root
Gitlab.config.gitlab.relative_url_root.presence || '/'
end
end end
...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn include IgnorableColumn
include ChronicDurationAttribute
prepend EE::ApplicationSetting prepend EE::ApplicationSetting
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
...@@ -46,6 +47,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -46,6 +47,8 @@ class ApplicationSetting < ActiveRecord::Base
default_value_for :id, 1 default_value_for :id, 1
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :uuid, presence: true validates :uuid, presence: true
validates :session_expire_delay, validates :session_expire_delay,
...@@ -185,6 +188,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -185,6 +188,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :archive_builds_in_seconds,
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -442,6 +449,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -442,6 +449,10 @@ class ApplicationSetting < ActiveRecord::Base
latest_terms latest_terms
end end
def archive_builds_older_than
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
private private
def ensure_uuid! def ensure_uuid!
......
...@@ -258,17 +258,37 @@ module Ci ...@@ -258,17 +258,37 @@ module Ci
.fabricate! .fabricate!
end end
def other_actions def other_manual_actions
pipeline.manual_actions.where.not(name: name) pipeline.manual_actions.where.not(name: name)
end end
def other_scheduled_actions
pipeline.scheduled_actions.where.not(name: name)
end
def pages_generator? def pages_generator?
Gitlab.config.pages.enabled && Gitlab.config.pages.enabled &&
self.name == 'pages' self.name == 'pages'
end end
# degenerated build is one that cannot be run by Runner
def degenerated?
self.options.nil?
end
def degenerate!
self.update!(options: nil, yaml_variables: nil, commands: nil)
end
def archived?
return true if degenerated?
archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
archive_builds_older_than.present? && created_at < archive_builds_older_than
end
def playable? def playable?
action? && (manual? || scheduled? || retryable?) action? && !archived? && (manual? || scheduled? || retryable?)
end end
def schedulable? def schedulable?
...@@ -296,7 +316,7 @@ module Ci ...@@ -296,7 +316,7 @@ module Ci
end end
def retryable? def retryable?
success? || failed? || canceled? !archived? && (success? || failed? || canceled?)
end end
def retries_count def retries_count
...@@ -304,7 +324,7 @@ module Ci ...@@ -304,7 +324,7 @@ module Ci
end end
def retries_max def retries_max
self.options.fetch(:retry, 0).to_i self.options.to_h.fetch(:retry, 0).to_i
end end
def latest? def latest?
......
...@@ -53,7 +53,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -53,7 +53,8 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5, missing_dependency_failure: 5,
runner_unsupported: 6, runner_unsupported: 6,
stale_schedule: 7, stale_schedule: 7,
job_execution_timeout: 8 job_execution_timeout: 8,
archived_failure: 9
}.merge(EE_FAILURE_REASONS) }.merge(EE_FAILURE_REASONS)
## ##
...@@ -169,16 +170,18 @@ class CommitStatus < ActiveRecord::Base ...@@ -169,16 +170,18 @@ class CommitStatus < ActiveRecord::Base
false false
end end
# To be overridden when inherrited from
def retryable? def retryable?
false false
end end
# To be overridden when inherrited from
def cancelable? def cancelable?
false false
end end
def archived?
false
end
def stuck? def stuck?
false false
end end
......
...@@ -89,7 +89,11 @@ class Deployment < ActiveRecord::Base ...@@ -89,7 +89,11 @@ class Deployment < ActiveRecord::Base
end end
def manual_actions def manual_actions
@manual_actions ||= deployable.try(:other_actions) @manual_actions ||= deployable.try(:other_manual_actions)
end
def scheduled_actions
@scheduled_actions ||= deployable.try(:other_scheduled_actions)
end end
def includes_commit?(commit) def includes_commit?(commit)
......
...@@ -119,6 +119,8 @@ class Note < ActiveRecord::Base ...@@ -119,6 +119,8 @@ class Note < ActiveRecord::Base
case notes_filter case notes_filter
when UserPreference::NOTES_FILTERS[:only_comments] when UserPreference::NOTES_FILTERS[:only_comments]
user user
when UserPreference::NOTES_FILTERS[:only_activity]
system
else else
all all
end end
......
...@@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base ...@@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base
# We could use enums, but Rails 4 doesn't support multiple # We could use enums, but Rails 4 doesn't support multiple
# enum options with same name for multiple fields, also it creates # enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here. # extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
belongs_to :user belongs_to :user
...@@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base ...@@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base
def notes_filters def notes_filters
{ {
s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes], s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes],
s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments] s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments],
s_('Notes|Show history only') => NOTES_FILTERS[:only_activity]
} }
end end
end end
......
...@@ -22,12 +22,17 @@ module Ci ...@@ -22,12 +22,17 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref) @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end end
condition(:archived, scope: :subject) do
@subject.archived?
end
condition(:terminal, scope: :subject) do condition(:terminal, scope: :subject) do
@subject.has_terminal? @subject.has_terminal?
end end
rule { protected_ref }.policy do rule { protected_ref | archived }.policy do
prevent :update_build prevent :update_build
prevent :update_commit_status
prevent :erase_build prevent :erase_build
end end
......
...@@ -2,4 +2,13 @@ ...@@ -2,4 +2,13 @@
class DeploymentPolicy < BasePolicy class DeploymentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:can_retry_deployable) do
can?(:update_build, @subject.deployable)
end
rule { ~can_retry_deployable }.policy do
prevent :create_deployment
prevent :update_deployment
end
end end
...@@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
missing_dependency_failure: 'There has been a missing dependency failure', missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner', runner_unsupported: 'Your runner is outdated, please upgrade your runner',
stale_schedule: 'Delayed job could not be executed by some reason, please try again', stale_schedule: 'Delayed job could not be executed by some reason, please try again',
job_execution_timeout: 'The script exceeded the maximum execution time set for the job' job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
archived_failure: 'The job is archived and cannot be run'
}.freeze }.freeze
private_constant :CALLOUT_FAILURE_MESSAGES private_constant :CALLOUT_FAILURE_MESSAGES
...@@ -31,6 +32,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -31,6 +32,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end end
def unrecoverable? def unrecoverable?
script_failure? || missing_dependency_failure? script_failure? || missing_dependency_failure? || archived_failure?
end end
end end
...@@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity ...@@ -25,4 +25,5 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :deployable, using: JobEntity expose :deployable, using: JobEntity
expose :manual_actions, using: JobEntity expose :manual_actions, using: JobEntity
expose :scheduled_actions, using: JobEntity
end end
...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity
expose :name expose :name
expose :started?, as: :started expose :started?, as: :started
expose :archived?, as: :archived
expose :build_path do |build| expose :build_path do |build|
build_path(build) build_path(build)
......
...@@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity ...@@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity
expose :notes_filters do |user_preference| expose :notes_filters do |user_preference|
UserPreference.notes_filters UserPreference.notes_filters
end end
expose :default_notes_filter do |user_preference|
UserPreference::NOTES_FILTERS[:all_notes]
end
end end
...@@ -84,6 +84,11 @@ module Ci ...@@ -84,6 +84,11 @@ module Ci
return false return false
end end
if build.archived?
build.drop!(:archived_failure)
return false
end
build.run! build.run!
true true
end end
......
...@@ -44,5 +44,13 @@ ...@@ -44,5 +44,13 @@
The default unit is in seconds, but you can define an alternative. For example: The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>. <code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
= f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
.form-text.text-muted
Set the duration when build gonna be considered old. Archived builds cannot be retried.
Make it empty to never expire builds. It has to be larger than 1 day.
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last? - if deployment.last?
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab %li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do = tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion Discussion
%span.badge.badge-pill= @merge_request.related_notes.user.count %span.badge.badge-pill= @merge_request.related_notes.user.count
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
= tab_link_for @merge_request, :pipelines do = tab_link_for @merge_request, :pipelines do
Pipelines Pipelines
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab %li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do = tab_link_for @merge_request, :diffs do
Changes Changes
%span.badge.badge-pill= @merge_request.diff_size %span.badge.badge-pill= @merge_request.diff_size
......
...@@ -61,8 +61,10 @@ ...@@ -61,8 +61,10 @@
%td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
= build.present.callout_failure_message = build.present.callout_failure_message
%td.responsive-table-cell.build-actions %td.responsive-table-cell.build-actions
- if can?(current_user, :update_build, job)
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
= icon('repeat') = icon('repeat')
- if can?(current_user, :read_build, job)
%tr.build-trace-row.responsive-table-border-end %tr.build-trace-row.responsive-table-border-end
%td %td
%td.responsive-table-cell.build-trace-container{ colspan: 4 } %td.responsive-table-cell.build-trace-container{ colspan: 4 }
......
- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id)
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- file_name = blob_item.name
= link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do
%span= file_name
- if is_lfs_blob
%span.badge.label-lfs.prepend-left-5 LFS
%td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.cgray.text-right
= render 'projects/tree/spinner'
%span.log_loading.hide
%i.fa.fa-spinner.fa-spin
Loading commit data...
%tr.tree-item
%td.tree-item-file-name
%i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
%td
%td.d-none.d-sm-table-cell
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- path = flatten_tree(@path, tree_item)
= link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do
%span= path
%td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.text-right
= render 'projects/tree/spinner'
- if tree_row.type == :tree - tree_row_name = tree_row.name
= render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } - tree_row_type = tree_row.type
- elsif tree_row.type == :blob
= render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } %tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" }
- elsif tree_row.type == :commit %td.tree-item-file-name
= render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' - if tree_row_type == :tree
= tree_icon('folder', tree_row.mode, tree_row.name)
- path = flatten_tree(@path, tree_row)
%a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path }
%span= path
- elsif tree_row_type == :blob
= tree_icon('file', tree_row.mode, tree_row_name)
%a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name }
%span= tree_row_name
- if @lfs_blob_ids.include?(tree_row.id)
%span.badge.label-lfs.prepend-left-5 LFS
- elsif tree_row_type == :commit
= tree_icon('archive', tree_row.mode, tree_row.name)
= submodule_link(tree_row, @ref)
%td.d-none.d-sm-table-cell.tree-commit
%td.tree-time-ago.text-right
%span.log_loading.hide
%i.fa.fa-spinner.fa-spin
Loading commit data...
---
title: Uses gitlab-ui components in jobs components
merge_request:
author:
type: other
---
title: Bump KUBERNETES_VERSION for Auto DevOps to latest 1.10 series
merge_request: 22757
author:
type: other
---
title: Soft-archive old jobs
merge_request:
author:
type: added
---
title: Improve performance of tree rendering in repositories with lots of items
merge_request:
author:
type: performance
---
title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb
merge_request:
author: gfyoung
type: performance
---
title: Add 'only history' option to notes filter
merge_request:
author:
type: changed
---
title: Add the Play button for delayed jobs in environment page
merge_request: 22106
author:
type: added
---
title: Align toggle sidebar button across all browsers and OSs
merge_request: 22771
author:
type: fixed
---
title: Bump Gitaly to 0.128.0
merge_request:
author:
type: added
# frozen_string_literal: true
class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true)
end
end
...@@ -211,6 +211,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do ...@@ -211,6 +211,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size" t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -16,7 +16,7 @@ query. ...@@ -16,7 +16,7 @@ query.
## Can I git push to a secondary node? ## Can I git push to a secondary node?
Yes, you can push changes to a **secondary** node. The push will be proxied to the **primary** node. Yes! Pushing directly to a **secondary** node (for both HTTP and SSH, including git-lfs) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
## How long does it take to have a commit replicated to a secondary node? ## How long does it take to have a commit replicated to a secondary node?
......
This diff is collapsed.
...@@ -2,22 +2,17 @@ ...@@ -2,22 +2,17 @@
# Using a Geo Server # Using a Geo Server
After you set up the [database replication and configure the Geo nodes][req], After you set up the [database replication and configure the Geo nodes][req], use your closest GitLab node as you would a normal standalone GitLab instance.
there are a few things to consider:
1. Users need an extra step to be able to fetch code from the secondary and push Pushing directly to a **secondary** node (for both HTTP, SSH including git-lfs) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
to primary:
1. Clone the repository as you would normally do, but from the secondary node: Example of the output you will see when pushing to a **secondary** node:
```bash ```bash
git clone git@secondary.gitlab.example.com:user/repo.git $ git push
``` > GitLab: You're pushing to a Geo secondary.
> GitLab: We'll help you by proxying this request to the primary: ssh://git@primary.geo/user/repo.git
1. Change the remote push URL to always push to primary, following this example: Everything up-to-date
```
```bash
git remote set-url --push origin git@primary.gitlab.example.com:user/repo.git
```
[req]: index.md#setup-instructions [req]: index.md#setup-instructions
...@@ -7,14 +7,14 @@ in GitLab 11.3. To learn how to use ...@@ -7,14 +7,14 @@ in GitLab 11.3. To learn how to use
When the GitLab Maven Repository is enabled, every project in GitLab will have When the GitLab Maven Repository is enabled, every project in GitLab will have
its own space to store [Maven](https://maven.apache.org/) packages. its own space to store [Maven](https://maven.apache.org/) packages.
To learn how to use it, see the [user documentation](../user/project/packages/maven.md). To learn how to use it, see the [user documentation](../user/project/packages/maven_repository.md).
## Enabling the Maven Repository ## Enabling the Maven Repository
NOTE: **Note:** NOTE: **Note:**
Once enabled, newly created projects will have the Packages feature enabled by Once enabled, newly created projects will have the Packages feature enabled by
default. Existing projects will need to default. Existing projects will need to
[explicitly enabled it](../user/project/packages/maven.md#enabling-the-packages-repository). [explicitly enabled it](../user/project/packages/maven_repository.md#enabling-the-packages-repository).
To enable the Maven repository: To enable the Maven repository:
......
...@@ -26,6 +26,7 @@ Gets all epics of the requested group and its subgroups. ...@@ -26,6 +26,7 @@ Gets all epics of the requested group and its subgroups.
GET /groups/:id/epics GET /groups/:id/epics
GET /groups/:id/epics?author_id=5 GET /groups/:id/epics?author_id=5
GET /groups/:id/epics?labels=bug,reproduced GET /groups/:id/epics?labels=bug,reproduced
GET /groups/:id/epics?state=opened
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -36,6 +37,7 @@ GET /groups/:id/epics?labels=bug,reproduced ...@@ -36,6 +37,7 @@ GET /groups/:id/epics?labels=bug,reproduced
| `order_by` | string | no | Return epics ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `order_by` | string | no | Return epics ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return epics sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return epics sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search epics against their `title` and `description` | | `search` | string | no | Search epics against their `title` and `description` |
| `state` | string | no | Search epics against their `state`, possible filters: `opened`, `closed` and `all`, default: `all` |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/epics
......
...@@ -94,6 +94,7 @@ The following table depicts the various user permission levels in a project. ...@@ -94,6 +94,7 @@ The following table depicts the various user permission levels in a project.
| Manage GitLab Pages | | | | ✓ | ✓ | | Manage GitLab Pages | | | | ✓ | ✓ |
| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ | | Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
| Remove GitLab Pages | | | | | ✓ | | Remove GitLab Pages | | | | | ✓ |
| View GitLab Pages protected by [access control](../administration/pages/index.md#access-control) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Manage clusters | | | | ✓ | ✓ | | Manage clusters | | | | ✓ | ✓ |
| Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ | | Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ | | Edit comments (posted by any user) | | | | ✓ | ✓ |
......
...@@ -66,7 +66,7 @@ export default { ...@@ -66,7 +66,7 @@ export default {
<button <button
ref="dropdown" ref="dropdown"
type="button" type="button"
class="btn btn-success review-preview-dropdown-toggle" class="btn btn-success review-preview-dropdown-toggle qa-review-preview-toggle"
@click="toggleReviewDropdown" @click="toggleReviewDropdown"
> >
{{ __('Finish review') }} {{ __('Finish review') }}
......
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
<template> <template>
<loading-button <loading-button
:loading="isPublishing" :loading="isPublishing"
container-class="btn btn-success js-publish-draft-button" container-class="btn btn-success js-publish-draft-button qa-submit-review"
@click="onClick" @click="onClick"
> >
<span> <span>
......
...@@ -46,13 +46,13 @@ export default { ...@@ -46,13 +46,13 @@ export default {
<template> <template>
<div v-show="draftsCount > 0"> <div v-show="draftsCount > 0">
<nav class="review-bar-component"> <nav class="review-bar-component">
<div class="review-bar-content"> <div class="review-bar-content qa-review-bar">
<preview-dropdown /> <preview-dropdown />
<loading-button <loading-button
v-gl-modal="$options.modalId" v-gl-modal="$options.modalId"
:loading="isDiscarding" :loading="isDiscarding"
:label="__('Discard review')" :label="__('Discard review')"
class="float-right" class="qa-discard-review float-right"
/> />
</div> </div>
</nav> </nav>
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
:ok-title="s__('BatchComments|Delete all pending comments')" :ok-title="s__('BatchComments|Delete all pending comments')"
:modal-id="$options.modalId" :modal-id="$options.modalId"
title-tag="h4" title-tag="h4"
ok-variant="danger" ok-variant="danger qa-modal-delete-pending-comments"
@ok="discardReview" @ok="discardReview"
> >
<p v-html="$options.text"> <p v-html="$options.text">
......
import $ from 'jquery';
import Mousetrap from 'mousetrap';
import Cookies from 'js-cookie';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
export default class ShortcutsEpic extends ShortcutsIssuable {
constructor() {
super();
const $issuableSidebar = $('.js-issuable-update');
Mousetrap.bind('l', () =>
ShortcutsEpic.openSidebarDropdown($issuableSidebar.find('.js-labels-block')),
);
Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
}
static openSidebarDropdown($block) {
if (Cookies.get('collapsed_gutter') === 'true') {
document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
} else {
$block.find('.js-sidebar-dropdown-toggle').trigger('click');
}
}
}
import $ from 'jquery'; import $ from 'jquery';
import Mousetrap from 'mousetrap';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
...@@ -7,10 +6,6 @@ export default class SidebarContext { ...@@ -7,10 +6,6 @@ export default class SidebarContext {
constructor() { constructor() {
const $issuableSidebar = $('.js-issuable-update'); const $issuableSidebar = $('.js-issuable-update');
Mousetrap.bind('l', () =>
SidebarContext.openSidebarDropdown($issuableSidebar.find('.js-labels-block')),
);
$issuableSidebar $issuableSidebar
.off('click', '.js-sidebar-dropdown-toggle') .off('click', '.js-sidebar-dropdown-toggle')
.on('click', '.js-sidebar-dropdown-toggle', function onClickEdit(e) { .on('click', '.js-sidebar-dropdown-toggle', function onClickEdit(e) {
...@@ -46,8 +41,4 @@ export default class SidebarContext { ...@@ -46,8 +41,4 @@ export default class SidebarContext {
} }
}); });
} }
static openSidebarDropdown($block) {
$block.find('.js-sidebar-dropdown-toggle').trigger('click');
}
} }
...@@ -226,9 +226,17 @@ export default { ...@@ -226,9 +226,17 @@ export default {
}, },
mounted() { mounted() {
eventHub.$on('toggleSidebar', this.toggleSidebar); eventHub.$on('toggleSidebar', this.toggleSidebar);
document.addEventListener(
'toggleSidebarRevealLabelsDropdown',
this.toggleSidebarRevealLabelsDropdown,
);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('toggleSidebar', this.toggleSidebar); eventHub.$off('toggleSidebar', this.toggleSidebar);
document.removeEventListener(
'toggleSidebarRevealLabelsDropdown',
this.toggleSidebarRevealLabelsDropdown,
);
}, },
methods: { methods: {
getDateValidity(startDate, endDate) { getDateValidity(startDate, endDate) {
......
...@@ -72,10 +72,10 @@ export default { ...@@ -72,10 +72,10 @@ export default {
<commit <commit
:commit-ref="commitRef" :commit-ref="commitRef"
:short-sha="project.last_deployment.commit.short_id" :short-sha="project.last_deployment.commit.short_id"
:commit-url="project.last_deployment.commit.web_url" :commit-url="project.last_deployment.commit.commit_url"
:title="project.last_deployment.commit.title" :title="project.last_deployment.commit.title"
:author="author" :author="author"
:tag="project.last_deployment.commit.tag" :tag="project.last_deployment.tag"
/> />
</div> </div>
<div <div
......
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import initEpicShow from 'ee/epics/epic_show/epic_show_bundle'; import initEpicShow from 'ee/epics/epic_show/epic_show_bundle';
import ShortcutsEpic from 'ee/behaviors/shortcuts/shortcuts_epic';
import '~/notes/index'; import '~/notes/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initEpicShow(); initEpicShow();
new ShortcutsEpic(); // eslint-disable-line no-new
}); });
...@@ -90,6 +90,7 @@ export default { ...@@ -90,6 +90,7 @@ export default {
>{{ vulnerability.name }}</span> >{{ vulnerability.name }}</span>
<vulnerability-issue-link <vulnerability-issue-link
v-if="hasIssue" v-if="hasIssue"
class="prepend-left-10"
:issue="vulnerability.issue_feedback" :issue="vulnerability.issue_feedback"
:project-name="vulnerability.project.name" :project-name="vulnerability.project.name"
/> />
......
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import VulnerabilityCount from './vulnerability_count.vue'; import VulnerabilityCount from './vulnerability_count.vue';
import { CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN } from '../store/modules/vulnerabilities/constants'; import { CRITICAL, HIGH, MEDIUM, LOW } from '../store/modules/vulnerabilities/constants';
const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN]; const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW];
export default { export default {
name: 'VulnerabilityCountList', name: 'VulnerabilityCountList',
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
<div class="d-inline"> <div class="d-inline">
<icon <icon
v-gl-tooltip v-gl-tooltip
name="issues" name="issue-created"
css-classes="text-success vertical-align-middle" css-classes="text-success vertical-align-middle"
:title="s__('Security Dashboard|Issue Created')" :title="s__('Security Dashboard|Issue Created')"
/> />
......
...@@ -2,4 +2,3 @@ export const CRITICAL = 'critical'; ...@@ -2,4 +2,3 @@ export const CRITICAL = 'critical';
export const HIGH = 'high'; export const HIGH = 'high';
export const MEDIUM = 'medium'; export const MEDIUM = 'medium';
export const LOW = 'low'; export const LOW = 'low';
export const UNKNOWN = 'unknown';
...@@ -70,13 +70,13 @@ ...@@ -70,13 +70,13 @@
.form-group.reset-approvals-on-push .form-group.reset-approvals-on-push
.form-check .form-check
= form.check_box :reset_approvals_on_push, class: 'form-check-input' = form.check_box :reset_approvals_on_push, class: 'form-check-input'
= form.label :reset_approvals_on_push do = form.label :reset_approvals_on_push, class: 'form-check-label' do
%strong Remove all approvals in a merge request when new commits are pushed to its source branch %strong Remove all approvals in a merge request when new commits are pushed to its source branch
.form-group.self-approval .form-group.self-approval
.form-check .form-check
= form.check_box :merge_requests_author_approval, class: 'form-check-input' = form.check_box :merge_requests_author_approval, class: 'form-check-input'
= form.label :merge_requests_author_approval do = form.label :merge_requests_author_approval, class: 'form-check-label' do
%strong Enable self approval of merge requests %strong Enable self approval of merge requests
= link_to icon('question-circle'), help_page_path("user/project/merge_requests/merge_request_approvals", = link_to icon('question-circle'), help_page_path("user/project/merge_requests/merge_request_approvals",
anchor: 'allowing-merge-request-authors-to-approve-their-own-merge-requests'), target: '_blank' anchor: 'allowing-merge-request-authors-to-approve-their-own-merge-requests'), target: '_blank'
......
---
title: Add 'l', 'r' and 'e' keyboard shortcuts support in Epic
merge_request: 8203
author:
type: added
---
title: Removes extra rigth margin from job page
merge_request:
author:
type: fixed
---
title: Filter epics by state in API
merge_request: 8179
author:
type: added
---
title: Link project short SHA to commit url
merge_request: 8214
author:
type: fixed
...@@ -58,6 +58,8 @@ module API ...@@ -58,6 +58,8 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'desc', optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return epics sorted in `asc` or `desc` order.' desc: 'Return epics sorted in `asc` or `desc` order.'
optional :search, type: String, desc: 'Search epics for text present in the title or description' optional :search, type: String, desc: 'Search epics for text present in the title or description'
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all epics'
optional :author_id, type: Integer, desc: 'Return epics which are authored by the user with the given ID' optional :author_id, type: Integer, desc: 'Return epics which are authored by the user with the given ID'
optional :labels, type: String, desc: 'Comma-separated list of label names' optional :labels, type: String, desc: 'Comma-separated list of label names'
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Epic shortcuts', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:label) { create(:group_label, group: group, title: 'bug') }
let(:note_text) { 'I got this!' }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
This is a task list:
- [ ] Incomplete entry 1
MARKDOWN
end
let(:epic) { create(:epic, group: group, title: 'make tea', description: markdown) }
before do
group.add_developer(user)
stub_licensed_features(epics: true)
sign_in(user)
visit group_epic_path(group, epic)
end
describe 'pressing "l"' do
it "opens labels dropdown for editing" do
find('body').native.send_key('l')
expect(find('.js-labels-block')).to have_selector('.dropdown-menu-labels.show')
end
end
describe 'pressing "r"' do
before do
create(:note, noteable: epic, note: note_text)
visit group_epic_path(group, epic)
wait_for_requests
end
it "quotes the selected text" do
select_element('.note-text')
find('body').native.send_key('r')
expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
end
end
describe 'pressing "e"' do
it "starts editing mode for epic" do
find('body').native.send_key('e')
expect(find('.detail-page-description')).to have_selector('form input#issuable-title')
expect(find('.detail-page-description')).to have_selector('form textarea#issue-description')
end
end
end
...@@ -45,8 +45,8 @@ describe 'Environments page', :js do ...@@ -45,8 +45,8 @@ describe 'Environments page', :js do
end end
it 'shows an enabled play button' do it 'shows an enabled play button' do
find('.js-dropdown-play-icon-container').click find('.js-environment-actions-dropdown').click
play_button = %q{button[class="js-manual-action-link no-btn btn"]} play_button = %q{button.js-manual-action-link.no-btn.btn}
expect(page).to have_selector(play_button) expect(page).to have_selector(play_button)
end end
...@@ -129,8 +129,8 @@ describe 'Environments page', :js do ...@@ -129,8 +129,8 @@ describe 'Environments page', :js do
end end
it 'show a disabled play button' do it 'show a disabled play button' do
find('.js-dropdown-play-icon-container').click find('.js-environment-actions-dropdown').click
disabled_play_button = %q{button[class="js-manual-action-link no-btn btn disabled"]} disabled_play_button = %q{button.js-manual-action-link.no-btn.btn.disabled}
expect(page).to have_selector(disabled_play_button) expect(page).to have_selector(disabled_play_button)
end end
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from 'ee/operations/store/index'; import store from 'ee/operations/store/index';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import Dashboard from 'ee/operations/components/dashboard/dashboard.vue'; import Dashboard from 'ee/operations/components/dashboard/dashboard.vue';
...@@ -12,24 +14,28 @@ describe('dashboard component', () => { ...@@ -12,24 +14,28 @@ describe('dashboard component', () => {
const ProjectSearchComponent = Vue.extend(ProjectSearch); const ProjectSearchComponent = Vue.extend(ProjectSearch);
const DashboardProjectComponent = Vue.extend(DashboardProject); const DashboardProjectComponent = Vue.extend(DashboardProject);
const projectTokens = mockProjectData(1); const projectTokens = mockProjectData(1);
const mockListPath = 'mock-listPath';
const mount = () => const mount = () =>
mountComponentWithStore(DashboardComponent, { mountComponentWithStore(DashboardComponent, {
store, store,
props: { props: {
addPath: 'mock-addPath', addPath: 'mock-addPath',
listPath: 'mock-listPath', listPath: mockListPath,
emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg', emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg',
}, },
}); });
let vm; let vm;
let mockAxios;
beforeEach(() => { beforeEach(() => {
vm = mount(); vm = mount();
mockAxios = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
clearState(store); clearState(store);
mockAxios.restore();
}); });
it('renders dashboard title', () => { it('renders dashboard title', () => {
...@@ -94,11 +100,9 @@ describe('dashboard component', () => { ...@@ -94,11 +100,9 @@ describe('dashboard component', () => {
}); });
describe('empty state', () => { describe('empty state', () => {
beforeAll(done => { beforeEach(() => {
vm.$store mockAxios.onGet(mockListPath).replyOnce(200, { data: [] });
.dispatch('requestProjects') vm = mount();
.then(() => vm.$nextTick(done))
.catch(done.fail);
}); });
it('renders empty state svg after requesting projects with no results', () => { it('renders empty state svg after requesting projects with no results', () => {
......
...@@ -74,8 +74,8 @@ describe('project component', () => { ...@@ -74,8 +74,8 @@ describe('project component', () => {
expect(commit.shortSha).toBe(vm.project.last_deployment.commit.short_id); expect(commit.shortSha).toBe(vm.project.last_deployment.commit.short_id);
}); });
it('binds web_url to commitUrl', () => { it('binds commitUrl', () => {
expect(commit.commitUrl).toBe(vm.project.last_deployment.commit.web_url); expect(commit.commitUrl).toBe(vm.project.last_deployment.commit.commit_url);
}); });
it('binds title', () => { it('binds title', () => {
...@@ -87,7 +87,7 @@ describe('project component', () => { ...@@ -87,7 +87,7 @@ describe('project component', () => {
}); });
it('binds tag', () => { it('binds tag', () => {
expect(commit.tag).toBe(vm.project.last_deployment.commit.tag); expect(commit.tag).toBe(vm.project.last_deployment.tag);
}); });
}); });
......
...@@ -34,10 +34,10 @@ export function mockProjectData( ...@@ -34,10 +34,10 @@ export function mockProjectData(
created_at: deployTimeStamp, created_at: deployTimeStamp,
commit: { commit: {
short_id: 'mock-short_id', short_id: 'mock-short_id',
tag: isTag,
title: 'mock-title', title: 'mock-title',
web_url: 'https://mock-web_url/', commit_url: 'https://mock-commit_url/',
}, },
tag: isTag,
user: { user: {
avatar_url: null, avatar_url: null,
path: 'mock-path', path: 'mock-path',
......
...@@ -92,6 +92,7 @@ describe API::Epics do ...@@ -92,6 +92,7 @@ describe API::Epics do
let!(:epic) do let!(:epic) do
create(:epic, create(:epic,
group: group, group: group,
state: :closed,
created_at: 3.days.ago, created_at: 3.days.ago,
updated_at: 2.days.ago) updated_at: 2.days.ago)
end end
...@@ -135,6 +136,18 @@ describe API::Epics do ...@@ -135,6 +136,18 @@ describe API::Epics do
expect_array_response([epic2.id]) expect_array_response([epic2.id])
end end
it 'returns epics matching given status' do
get api(url, user), state: :opened
expect_array_response([epic2.id])
end
it 'returns all epics when state set to all' do
get api(url, user), state: :all
expect_array_response([epic2.id, epic.id])
end
it 'sorts by created_at descending by default' do it 'sorts by created_at descending by default' do
get api(url, user) get api(url, user)
......
# frozen_string_literal: true
module Gitlab module Gitlab
module Ci module Ci
module Status module Status
......
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