Commit f5050253 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f6cdec67
......@@ -269,14 +269,6 @@ RSpec/ScatteredSetup:
- 'spec/requests/api/jobs_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
# Offense count: 4
RSpec/VoidExpect:
Exclude:
- 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb'
- 'spec/models/ci/group_spec.rb'
- 'spec/models/ci/runner_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
# Offense count: 10
# Cop supports --auto-correct.
Rails/ApplicationController:
......
......@@ -457,9 +457,9 @@ end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 12.9.0.pre.rc4'
gem 'grpc', '~> 1.27.0'
gem 'grpc', '~> 1.24.0'
gem 'google-protobuf', '~> 3.11.2'
gem 'google-protobuf', '~> 3.8.0'
gem 'toml-rb', '~> 1.0.0'
......
......@@ -427,7 +427,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.11.4)
google-protobuf (3.8.0)
googleapis-common-protos-types (1.0.4)
google-protobuf (~> 3.0)
googleauth (0.6.6)
......@@ -468,8 +468,8 @@ GEM
graphql (~> 1.6)
html-pipeline (~> 2.8)
sass (~> 3.4)
grpc (1.27.0)
google-protobuf (~> 3.11)
grpc (1.24.0)
google-protobuf (~> 3.8)
googleapis-common-protos-types (~> 1.0)
gssapi (1.2.0)
ffi (>= 1.0.1)
......@@ -1251,7 +1251,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.11.2)
google-protobuf (~> 3.8.0)
gpgme (~> 2.0.19)
grape (~> 1.1.0)
grape-entity (~> 0.7.1)
......@@ -1260,7 +1260,7 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.10.5)
graphql-docs (~> 1.6.0)
grpc (~> 1.27.0)
grpc (~> 1.24.0)
gssapi
guard-rspec
haml_lint (~> 0.34.0)
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return this.file.permalink;
},
},
};
</script>
<template>
<div v-if="showButtons" class="pull-right ide-btn-group">
<a
:href="file.permalink"
:title="s__('IDE|Open in file view')"
target="_blank"
rel="noopener noreferrer"
>
<span class="vertical-align-middle">{{ __('Open in file view') }}</span>
<icon :size="16" name="external-link" class="vertical-align-middle space-right" />
</a>
</div>
</template>
......@@ -56,7 +56,6 @@ export default {
required: true,
},
},
traceHeight: 600,
data() {
return {
isElasticStackCalloutDismissed: false,
......@@ -94,6 +93,9 @@ export default {
'showEnvironment',
'fetchEnvironments',
'fetchMoreLogsPrepend',
'dismissRequestEnvironmentsError',
'dismissInvalidTimeRangeWarning',
'dismissRequestLogsError',
]),
isCurrentEnvironment(envName) {
......@@ -115,7 +117,7 @@ export default {
};
</script>
<template>
<div class="environment-logs-viewer mt-3">
<div class="environment-logs-viewer d-flex flex-column py-3">
<gl-alert
v-if="shouldShowElasticStackCallout"
class="mb-3 js-elasticsearch-alert"
......@@ -132,6 +134,31 @@ export default {
</strong>
</a>
</gl-alert>
<gl-alert
v-if="environments.fetchError"
class="mb-3"
variant="danger"
@dismiss="dismissRequestEnvironmentsError"
>
{{ s__('Metrics|There was an error fetching the environments data, please try again') }}
</gl-alert>
<gl-alert
v-if="timeRange.invalidWarning"
class="mb-3"
variant="warning"
@dismiss="dismissInvalidTimeRangeWarning"
>
{{ s__('Metrics|Invalid time range, please verify.') }}
</gl-alert>
<gl-alert
v-if="logs.fetchError"
class="mb-3"
variant="danger"
@dismiss="dismissRequestLogsError"
>
{{ s__('Environments|There was an error fetching the logs. Please try again.') }}
</gl-alert>
<div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="flex-grow-0">
<gl-dropdown
......@@ -183,16 +210,15 @@ export default {
<gl-infinite-scroll
ref="infiniteScroll"
class="log-lines"
:style="{ height: `${$options.traceHeight}px` }"
:max-list-height="$options.traceHeight"
class="log-lines overflow-auto flex-grow-1 min-height-0"
:fetched-items="logs.lines.length"
@topReached="topReached"
@scroll="scroll"
>
<template #items>
<pre
class="build-trace js-log-trace"
ref="logTrace"
class="build-trace"
><code class="bash js-build-output"><div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
......@@ -205,7 +231,7 @@ export default {
></template>
</gl-infinite-scroll>
<div ref="logFooter" class="log-footer py-2 px-3">
<div ref="logFooter" class="py-2 px-3 text-white bg-secondary-900">
<gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')">
<template #start>{{ timeRange.current.start | formatDate }}</template>
<template #end>{{ timeRange.current.end | formatDate }}</template>
......
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { s__ } from '~/locale';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import * as types from './mutation_types';
const flashTimeRangeWarning = () => {
flash(s__('Metrics|Invalid time range, please verify.'), 'warning');
};
const flashLogsError = () => {
flash(s__('Metrics|There was an error fetching the logs, please try again'));
};
const requestUntilData = (url, params) =>
backOff((next, stop) => {
axios
......@@ -31,7 +21,7 @@ const requestUntilData = (url, params) =>
});
});
const requestLogsUntilData = state => {
const requestLogsUntilData = ({ commit, state }) => {
const params = {};
const { logs_api_path } = state.environments.options.find(
({ name }) => name === state.environments.current,
......@@ -49,7 +39,7 @@ const requestLogsUntilData = state => {
params.start_time = start;
params.end_time = end;
} catch {
flashTimeRangeWarning();
commit(types.SHOW_TIME_RANGE_INVALID_WARNING);
}
}
if (state.logs.cursor) {
......@@ -101,26 +91,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
})
.catch(() => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR);
flash(s__('Metrics|There was an error fetching the environments data, please try again'));
});
};
export const fetchLogs = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData(state)
return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { pod_name, pods, logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
})
.catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR);
commit(types.RECEIVE_LOGS_DATA_ERROR);
flashLogsError();
});
};
......@@ -132,16 +118,27 @@ export const fetchMoreLogsPrepend = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA_PREPEND);
return requestLogsUntilData(state)
return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor });
})
.catch(() => {
commit(types.RECEIVE_LOGS_DATA_PREPEND_ERROR);
flashLogsError();
});
};
export const dismissRequestEnvironmentsError = ({ commit }) => {
commit(types.HIDE_REQUEST_ENVIRONMENTS_ERROR);
};
export const dismissRequestLogsError = ({ commit }) => {
commit(types.HIDE_REQUEST_LOGS_ERROR);
};
export const dismissInvalidTimeRangeWarning = ({ commit }) => {
commit(types.HIDE_TIME_RANGE_INVALID_WARNING);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH';
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export const SHOW_TIME_RANGE_INVALID_WARNING = 'SHOW_TIME_RANGE_INVALID_WARNING';
export const HIDE_TIME_RANGE_INVALID_WARNING = 'HIDE_TIME_RANGE_INVALID_WARNING';
export const SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR';
export const HIDE_REQUEST_ENVIRONMENTS_ERROR = 'HIDE_REQUEST_ENVIRONMENTS_ERROR';
export const REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA';
export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS';
......@@ -13,6 +18,7 @@ export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR';
export const REQUEST_LOGS_DATA_PREPEND = 'REQUEST_LOGS_DATA_PREPEND';
export const RECEIVE_LOGS_DATA_PREPEND_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS';
export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR';
export const HIDE_REQUEST_LOGS_ERROR = 'HIDE_REQUEST_LOGS_ERROR';
export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR';
......@@ -18,6 +18,12 @@ export default {
state.timeRange.selected = timeRange;
state.timeRange.current = convertToFixedRange(timeRange);
},
[types.SHOW_TIME_RANGE_INVALID_WARNING](state) {
state.timeRange.invalidWarning = true;
},
[types.HIDE_TIME_RANGE_INVALID_WARNING](state) {
state.timeRange.invalidWarning = false;
},
// Environments Data
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
......@@ -38,6 +44,10 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_ERROR](state) {
state.environments.options = [];
state.environments.isLoading = false;
state.environments.fetchError = true;
},
[types.HIDE_REQUEST_ENVIRONMENTS_ERROR](state) {
state.environments.fetchError = false;
},
// Logs data
......@@ -63,6 +73,7 @@ export default {
[types.RECEIVE_LOGS_DATA_ERROR](state) {
state.logs.lines = [];
state.logs.isLoading = false;
state.logs.fetchError = true;
},
[types.REQUEST_LOGS_DATA_PREPEND](state) {
......@@ -80,6 +91,10 @@ export default {
},
[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) {
state.logs.isLoading = false;
state.logs.fetchError = true;
},
[types.HIDE_REQUEST_LOGS_ERROR](state) {
state.logs.fetchError = false;
},
// Pods data
......
......@@ -16,6 +16,8 @@ export default () => ({
selected: defaultTimeRange,
// Current time range, must be fixed
current: convertToFixedRange(defaultTimeRange),
invalidWarning: false,
},
/**
......@@ -25,6 +27,7 @@ export default () => ({
options: [],
isLoading: false,
current: null,
fetchError: false,
},
/**
......@@ -39,6 +42,8 @@ export default () => ({
*/
cursor: null,
isComplete: false,
fetchError: false,
},
/**
......
......@@ -99,7 +99,17 @@ export default {
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
);
this.$emit('onClickTriggered', this.pipeline, pipeline);
/**
* If the expanded trigger is defined and the id is different than the
* pipeline we clicked, then it means we clicked on a sibling downstream link
* and we want to reset the pipeline store. Triggering the reset without
* this condition would mean not allowing downstreams of downstreams to expand
*/
if (this.expandedTriggered?.id !== pipeline.id) {
this.$emit('onResetTriggered', this.pipeline, pipeline);
}
this.$emit('onClickTriggered', pipeline);
},
calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
......@@ -136,9 +146,7 @@ export default {
:pipeline="expandedTriggeredBy"
:is-linked-pipeline="true"
:mediator="mediator"
@onClickTriggeredBy="
(parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
"
@onClickTriggeredBy="clickTriggeredByPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
......@@ -148,9 +156,7 @@ export default {
:column-title="__('Upstream')"
:project-id="pipelineProjectId"
graph-position="left"
@linkedPipelineClick="
linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
"
@linkedPipelineClick="$emit('onClickTriggeredBy', $event)"
/>
<ul
......@@ -197,9 +203,7 @@ export default {
:is-linked-pipeline="true"
:style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator"
@onClickTriggered="
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
"
@onClickTriggered="clickTriggeredPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</div>
......
......@@ -27,9 +27,9 @@ export default {
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) {
clickPipeline(pipeline, openMethod, closeMethod) {
if (!pipeline.isExpanded) {
this.mediator.store[openMethod](parentPipeline, pipeline);
this.mediator.store[openMethod](pipeline);
this.mediator.store.toggleLoading(pipeline);
this.mediator.poll.stop();
......@@ -41,21 +41,14 @@ export default {
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
}
},
clickTriggeredByPipeline(parentPipeline, pipeline) {
this.clickPipeline(
parentPipeline,
pipeline,
'openTriggeredByPipeline',
'closeTriggeredByPipeline',
);
resetTriggeredPipelines(parentPipeline, pipeline) {
this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
},
clickTriggeredPipeline(parentPipeline, pipeline) {
this.clickPipeline(
parentPipeline,
pipeline,
'openTriggeredPipeline',
'closeTriggeredPipeline',
);
clickTriggeredByPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
clickTriggeredPipeline(pipeline) {
this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
requestRefreshPipelineGraph() {
// When an action is clicked
......
......@@ -42,10 +42,10 @@ export default () => {
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
onClickTriggeredBy: (parentPipeline, pipeline) =>
this.clickTriggeredByPipeline(parentPipeline, pipeline),
onClickTriggered: (parentPipeline, pipeline) =>
this.clickTriggeredPipeline(parentPipeline, pipeline),
onResetTriggered: (parentPipeline, pipeline) =>
this.resetTriggeredPipelines(parentPipeline, pipeline),
onClickTriggeredBy: pipeline => this.clickTriggeredByPipeline(pipeline),
onClickTriggered: pipeline => this.clickTriggeredPipeline(pipeline),
},
});
},
......
......@@ -54,16 +54,24 @@ export default class PipelineStore {
*/
parseTriggeredByPipelines(oldPipeline = {}, newPipeline) {
// keep old value in case it's opened because we're polling
Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
// add isLoading property
Vue.set(newPipeline, 'isLoading', false);
// Because there can only ever be one `triggered_by` for any given pipeline,
// the API returns an object for the value instead of an Array. However,
// it's easier to deal with an array in the FE so we convert it.
if (newPipeline.triggered_by) {
if (!Array.isArray(newPipeline.triggered_by)) {
Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] });
}
this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]);
if (newPipeline.triggered_by?.length > 0) {
newPipeline.triggered_by.forEach(el => {
const oldTriggeredBy = oldPipeline.triggered_by?.find(element => element.id === el.id);
this.parseTriggeredPipelines(oldTriggeredBy, el);
});
}
}
}
......
<script>
import { escapeRegExp } from 'lodash';
import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import {
GlBadge,
GlLink,
GlSkeletonLoading,
GlTooltipDirective,
GlLoadingIcon,
GlIcon,
} from '@gitlab/ui';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { getIconName } from '../../utils/icon';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import getRefMixin from '../../mixins/get_ref';
import getCommit from '../../queries/getCommit.query.graphql';
......@@ -14,8 +20,9 @@ export default {
GlLink,
GlSkeletonLoading,
GlLoadingIcon,
GlIcon,
TimeagoTooltip,
Icon,
FileIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -95,9 +102,6 @@ export default {
? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` }
: null;
},
iconName() {
return `fa-${getIconName(this.type, this.path)}`;
},
isFolder() {
return this.type === 'tree';
},
......@@ -123,12 +127,6 @@ export default {
<template>
<tr class="tree-item">
<td class="tree-item-file-name cursor-default position-relative">
<gl-loading-icon
v-if="path === loadingPath"
size="sm"
inline
class="d-inline-block align-text-bottom fa-fw"
/>
<component
:is="linkComponent"
ref="link"
......@@ -140,27 +138,27 @@ export default {
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
>
<i
v-if="path !== loadingPath"
:aria-label="type"
role="img"
:class="iconName"
class="fa fa-fw mr-1"
></i
><span class="position-relative">{{ fullPath }}</span>
<file-icon
:file-name="fullPath"
:folder="isFolder"
:submodule="isSubmodule"
:loading="path === loadingPath"
css-classes="position-relative file-icon"
class="mr-1 position-relative text-secondary"
/><span class="position-relative">{{ fullPath }}</span>
</component>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge>
<template v-if="isSubmodule">
@ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link>
</template>
<icon
<gl-icon
v-if="hasLockLabel"
v-gl-tooltip
:title="commit.lockLabel"
name="lock"
:size="12"
class="ml-2 vertical-align-middle"
class="ml-1"
/>
</td>
<td class="d-none d-sm-table-cell tree-commit cursor-default">
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import getIconForFile from './file_icon/file_icon_map';
import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite
icon
......@@ -17,8 +16,8 @@ import icon from '../../vue_shared/components/icon.vue';
*/
export default {
components: {
icon,
GlLoadingIcon,
GlIcon,
},
props: {
fileName: {
......@@ -31,7 +30,11 @@ export default {
required: false,
default: false,
},
submodule: {
type: Boolean,
required: false,
default: false,
},
opened: {
type: Boolean,
required: false,
......@@ -58,7 +61,7 @@ export default {
},
computed: {
spriteHref() {
const iconName = getIconForFile(this.fileName) || 'file';
const iconName = this.submodule ? 'folder-git' : getIconForFile(this.fileName) || 'file';
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
......@@ -73,9 +76,12 @@ export default {
<template>
<span>
<svg v-if="!loading && !folder" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
<icon v-if="!loading && folder" :name="folderIconName" :size="size" class="folder-icon" />
<gl-loading-icon v-if="loading" :inline="true" />
<use v-bind="{ 'xlink:href': spriteHref }" /></svg
><gl-icon
v-if="!loading && folder"
:name="folderIconName"
:size="size"
class="folder-icon"
/><gl-loading-icon v-if="loading" :inline="true" />
</span>
</template>
......@@ -474,6 +474,9 @@ img.emoji {
.mw-70p { max-width: 70%; }
.mw-90p { max-width: 90%; }
// By default flex items don't shrink below their minimum content size.
// To change this, these clases set a min-width or min-height
.min-width-0 { min-width: 0; }
.min-height-0 { min-height: 0; }
.svg-w-100 {
......
......@@ -199,8 +199,8 @@
/*
* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
*/
@mixin build-trace {
background: $black;
@mixin build-trace($background: $black) {
background: $background;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
......@@ -243,7 +243,7 @@
/*
* 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, $svg-display: 'block', $svg-top: '2px') {
@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: block, $svg-top: 2px) {
display: flex;
font-size: $control-font-size;
justify-content: $flex-direction;
......
......@@ -641,6 +641,14 @@ $issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
*/
$environment-logs-breadcrumbs-height: 63px;
$environment-logs-breadcrumbs-height-md: $breadcrumb-min-height;
$environment-logs-difference-xs-up: $header-height + $environment-logs-breadcrumbs-height;
$environment-logs-difference-md-up: $header-height + $environment-logs-breadcrumbs-height-md;
/*
* Avatar
......
......@@ -356,54 +356,3 @@
}
}
}
.environment-logs-viewer {
.build-trace-container {
position: relative;
}
.log-lines,
.gl-infinite-scroll-container {
// makes scrollbar visible by creating contrast
background: $black;
}
.gl-infinite-scroll-legend {
margin: 0;
}
.build-trace {
@include build-trace();
margin: 0;
}
.top-bar {
.date-time-picker-wrapper,
.dropdown-toggle {
@include media-breakpoint-up(md) {
width: 140px;
}
@include media-breakpoint-up(lg) {
width: 160px;
}
}
.controllers {
@include build-controllers(16px, flex-end, false, 2);
}
}
.btn-refresh svg {
top: 0;
}
.build-loader-animation {
@include build-loader-animation;
}
.log-footer {
color: $white-normal;
background-color: $gray-900;
}
}
.environment-logs-page {
.content-wrapper {
padding-bottom: 0;
}
}
.environment-logs-viewer {
height: calc(100vh - #{$environment-logs-difference-xs-up});
min-height: 700px;
@include media-breakpoint-up(md) {
height: calc(100vh - #{$environment-logs-difference-md-up});
}
.with-performance-bar & {
height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height});
@include media-breakpoint-up(md) {
height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height});
}
}
.top-bar {
.date-time-picker-wrapper,
.dropdown-toggle {
@include media-breakpoint-up(md) {
width: 140px;
}
@include media-breakpoint-up(lg) {
width: 160px;
}
}
.controllers {
@include build-controllers(16px, flex-end, false, 2, inline);
}
}
.log-lines,
.gl-infinite-scroll-container {
// makes scrollbar visible by creating contrast
background: $black;
height: 100%;
}
.build-trace {
@include build-trace($black);
}
.gl-infinite-scroll-legend {
margin: 0;
}
.build-loader-animation {
@include build-loader-animation;
}
}
......@@ -138,6 +138,12 @@
}
.tree-item {
.file-icon,
.folder-icon {
position: relative;
top: 2px;
}
.link-container {
padding: 0;
......
......@@ -16,6 +16,10 @@ class Admin::DashboardController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
def stats
@users_statistics = UsersStatistics.latest
end
def show_license_breakdown?
false
end
......
......@@ -256,6 +256,7 @@ module ApplicationHelper
def page_class
class_names = []
class_names << 'issue-boards-page' if current_controller?(:boards)
class_names << 'environment-logs-page' if current_controller?(:logs)
class_names << 'with-performance-bar' if performance_bar_enabled?
class_names << system_message_class
class_names
......
......@@ -176,6 +176,7 @@ module ApplicationSettingsHelper
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
:container_expiration_policies_enable_historic_entries,
:container_registry_token_expire_delay,
:default_artifacts_expire_in,
:default_branch_protection,
......
......@@ -142,6 +142,9 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
inclusion: { in: [true, false], message: 'must be a boolean value' }
validates :container_registry_token_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......
......@@ -42,6 +42,7 @@ module ApplicationSettingImplementation
asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
container_expiration_policies_enable_historic_entries: false,
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
......
# frozen_string_literal: true
class UsersStatistics < ApplicationRecord
STATISTICS_NAMES = [
:without_groups_and_projects,
:with_highest_role_guest,
:with_highest_role_reporter,
:with_highest_role_developer,
:with_highest_role_maintainer,
:with_highest_role_owner,
:bots,
:blocked
].freeze
scope :order_created_at_desc, -> { order(created_at: :desc) }
class << self
def latest
order_created_at_desc.first
end
end
def active
[
without_groups_and_projects,
with_highest_role_guest,
with_highest_role_reporter,
with_highest_role_developer,
with_highest_role_maintainer,
with_highest_role_owner,
bots
].sum
end
def total
active + blocked
end
class << self
def create_current_stats!
......
......@@ -5,5 +5,14 @@
.form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-bold'
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
.form-group
.form-check
= f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input'
= f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do
= _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy')
.form-text.text-muted
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
= f.submit 'Save changes', class: "btn btn-success"
......@@ -30,7 +30,7 @@
%hr
.btn-group.d-flex{ role: 'group' }
= link_to 'New user', new_admin_user_path, class: "btn btn-success"
= render_if_exists 'admin/dashboard/users_statistics'
= link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary'
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
......
- page_title s_('AdminArea|Users statistics')
%h3.my-4
= s_('AdminArea|Users statistics')
%table.table.gl-text-gray-700
%tr
%td.p-3
= s_('AdminArea|Users without a Group and Project')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
%td.p-3.text-right
= @users_statistics&.without_groups_and_projects.to_i
%tr
%td.p-3
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Guest')
= render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
%td.p-3.text-right
= @users_statistics&.with_highest_role_guest.to_i
%tr
%td.p-3
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Reporter')
%td.p-3.text-right
= @users_statistics&.with_highest_role_reporter.to_i
%tr
%td.p-3
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Developer')
%td.p-3.text-right
= @users_statistics&.with_highest_role_developer.to_i
%tr
%td.p-3
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Maintainer')
%td.p-3.text-right
= @users_statistics&.with_highest_role_maintainer.to_i
%tr
%td.p-3
= s_('AdminArea|Users with highest role')
%strong
= s_('AdminArea|Owner')
%td.p-3.text-right
= @users_statistics&.with_highest_role_owner.to_i
%tr
%td.p-3
= s_('AdminArea|Bots')
%td.p-3.text-right
= @users_statistics&.bots.to_i
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%strong
= s_('AdminArea|Active users')
= render_if_exists 'admin/dashboard/billable_users_text'
%td.p-3.text-right
%strong
= @users_statistics&.active.to_i
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%strong
= s_('AdminArea|Blocked users')
%td.p-3.text-right
%strong
= @users_statistics&.blocked.to_i
%tr.bg-gray-light.gl-text-gray-900
%td.p-3
%strong
= s_('AdminArea|Total users')
%td.p-3.text-right
%strong
= @users_statistics&.total.to_i
---
title: Resolve Unable to expand multiple downstream pipelines.
merge_request: 27029
author:
type: fixed
---
title: Add application setting to enable container expiration and retention policies
on pre 12.8 projects
merge_request: 28479
author:
type: added
---
title: Enable log explorer to use the full height of the screen
merge_request: 28312
author:
type: added
---
title: Add status endpoint to Pages Internal API
merge_request: 28743
author:
type: added
---
title: Use rich icons for thw rows on the file tree
merge_request: 28112
author:
type: changed
---
title: Show user statistics in admin area also in CE, and use daily generated data for these statistics
merge_request: 27345
author:
type: changed
......@@ -161,5 +161,7 @@ namespace :admin do
concerns :clusterable
get '/dashboard/stats', to: 'dashboard#stats'
root to: 'dashboard#index'
end
# frozen_string_literal: true
class AddContainerExpirationPoliciesEnableHistoricEntriesToApplicationSettings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:application_settings,
:container_expiration_policies_enable_historic_entries,
:boolean,
default: false,
allow_null: false)
end
def down
remove_column(:application_settings,
:container_expiration_policies_enable_historic_entries)
end
end
......@@ -397,7 +397,8 @@ CREATE TABLE public.application_settings (
email_restrictions text,
npm_package_requests_forwarding boolean DEFAULT true NOT NULL,
namespace_storage_size_limit bigint DEFAULT 0 NOT NULL,
seat_link_enabled boolean DEFAULT true NOT NULL
seat_link_enabled boolean DEFAULT true NOT NULL,
container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE public.application_settings_id_seq
......@@ -13001,6 +13002,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200330121000
20200330123739
20200330132913
20200331195952
20200331220930
20200402123926
20200402135250
......
......@@ -516,6 +516,10 @@ on how to achieve that.
## Use an external container registry with GitLab as an auth endpoint
NOTE: **Note:**
In using an external container registry, some features associated with the
container registry may be unavailable or have [inherant risks](./../../user/packages/container_registry/index.md#use-with-external-container-registries)
**Omnibus GitLab**
You can use GitLab as an auth endpoint with an external container registry.
......
......@@ -2984,6 +2984,103 @@ type EpicTreeReorderPayload {
errors: [String!]!
}
type GeoNode {
"""
The maximum concurrency of container repository sync for this secondary node
"""
containerRepositoriesMaxCapacity: Int
"""
Indicates whether this Geo node is enabled
"""
enabled: Boolean
"""
The maximum concurrency of LFS/attachment backfill for this secondary node
"""
filesMaxCapacity: Int
"""
ID of this GeoNode
"""
id: ID!
"""
The URL defined on the primary node that secondary nodes should use to contact it
"""
internalUrl: String
"""
The interval (in days) in which the repository verification is valid. Once expired, it will be reverified
"""
minimumReverificationInterval: Int
"""
The unique identifier for this Geo node
"""
name: String
"""
Indicates whether this Geo node is the primary
"""
primary: Boolean
"""
The maximum concurrency of repository backfill for this secondary node
"""
reposMaxCapacity: Int
"""
The namespaces that should be synced, if `selective_sync_type` == `namespaces`
"""
selectiveSyncNamespaces(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): NamespaceConnection
"""
The repository storages whose projects should be synced, if `selective_sync_type` == `shards`
"""
selectiveSyncShards: [String!]
"""
Indicates if syncing is limited to only specific groups, or shards
"""
selectiveSyncType: String
"""
Indicates if this secondary node will replicate blobs in Object Storage
"""
syncObjectStorage: Boolean
"""
The user-facing URL for this Geo node
"""
url: String
"""
The maximum concurrency of repository verification for this secondary node
"""
verificationMaxCapacity: Int
}
type GrafanaIntegration {
"""
Timestamp of the issue's creation
......@@ -5435,6 +5532,41 @@ type Namespace {
visibility: String
}
"""
The connection type for Namespace.
"""
type NamespaceConnection {
"""
A list of edges.
"""
edges: [NamespaceEdge]
"""
A list of nodes.
"""
nodes: [Namespace]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type NamespaceEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Namespace
}
type Note {
"""
User who wrote this note
......@@ -6916,6 +7048,16 @@ type Query {
text: String!
): String!
"""
Find a Geo node
"""
geoNode(
"""
The name of the Geo node. Defaults to the current Geo node name.
"""
name: String
): GeoNode
"""
Find a group
"""
......
......@@ -483,6 +483,25 @@ Autogenerated return type of EpicTreeReorder
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
## GeoNode
| Name | Type | Description |
| --- | ---- | ---------- |
| `containerRepositoriesMaxCapacity` | Int | The maximum concurrency of container repository sync for this secondary node |
| `enabled` | Boolean | Indicates whether this Geo node is enabled |
| `filesMaxCapacity` | Int | The maximum concurrency of LFS/attachment backfill for this secondary node |
| `id` | ID! | ID of this GeoNode |
| `internalUrl` | String | The URL defined on the primary node that secondary nodes should use to contact it |
| `minimumReverificationInterval` | Int | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified |
| `name` | String | The unique identifier for this Geo node |
| `primary` | Boolean | Indicates whether this Geo node is the primary |
| `reposMaxCapacity` | Int | The maximum concurrency of repository backfill for this secondary node |
| `selectiveSyncShards` | String! => Array | The repository storages whose projects should be synced, if `selective_sync_type` == `shards` |
| `selectiveSyncType` | String | Indicates if syncing is limited to only specific groups, or shards |
| `syncObjectStorage` | Boolean | Indicates if this secondary node will replicate blobs in Object Storage |
| `url` | String | The user-facing URL for this Geo node |
| `verificationMaxCapacity` | Int | The maximum concurrency of repository verification for this secondary node |
## GrafanaIntegration
| Name | Type | Description |
......
......@@ -45,6 +45,7 @@ Example response:
"default_group_visibility" : "private",
"gravatar_enabled" : true,
"sign_in_text" : null,
"container_expiration_policies_enable_historic_entries": true,
"container_registry_token_expire_delay": 5,
"repository_storages": ["default"],
"plantuml_enabled": false,
......
......@@ -15,7 +15,21 @@ tag) with an API call.
## Authentication tokens
The following methods of authentication are supported.
The following methods of authentication are supported:
- [Trigger token](#trigger-token)
- [CI job token](#ci-job-token)
If using the `$CI_PIPELINE_SOURCE` [predefined environment variable](../variables/predefined_variables.md#variables-reference)
to limit which jobs run in a pipeline, the value could be either `pipeline` or `trigger`,
depending on which trigger method is used.
| `$CI_PIPELINE_SOURCE` value | Trigger method |
|-----------------------------|----------------|
| `pipeline` | Using the `trigger:` keyword in the CI/CD configuration file, or using the trigger API with `$CI_JOB_TOKEN`. |
| `trigger` | Using the trigger API using a generated trigger token |
This also applies when using the `pipelines` or `triggers` keywords with the legacy [`only/except` basic syntax](../yaml/README.md#onlyexcept-basic).
### Trigger token
......
......@@ -147,6 +147,9 @@ The **Total users** is calculated as: **Active users** + **Blocked users**.
GitLab billing is based on the number of active users. For details of active users, see
[Choosing the number of users](../../subscriptions/index.md#choosing-the-number-of-users).
**Please note** that during the initial stage, the information won't be 100% accurate given that
background processes are still recollecting data.
### Administering Groups
You can administer all groups in the GitLab instance from the Admin Area's Groups page.
......
......@@ -61,7 +61,7 @@ Access the default page for admin area settings by navigating to
| ------ | ----------- |
| [Continuous Integration and Deployment](continuous_integration.md) | Auto DevOps, runners and job artifacts. |
| [Required pipeline configuration](continuous_integration.md#required-pipeline-configuration-premium-only) **(PREMIUM ONLY)** | Set an instance-wide auto included [pipeline configuration](../../../ci/yaml/README.md). This pipeline configuration will be run after the project's own configuration. |
| [Package Registry](continuous_integration.md#package-registry-configuration-premium-only) **(PREMIUM ONLY)**| Settings related to the use and experience of using GitLab's Package Registry. |
| [Package Registry](continuous_integration.md#package-registry-configuration-premium-only) **(PREMIUM ONLY)**| Settings related to the use and experience of using GitLab's Package Registry. Note there are [risks involved](./../../packages/container_registry/index.md#use-with-external-container-registries) in enabling some of these settings. |
## Reporting
......
......@@ -488,7 +488,9 @@ older tags and images are regularly removed from the Container Registry.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15398) in GitLab 12.8.
NOTE: **Note:**
Expiration policies are only available for projects created in GitLab 12.8 and later.
Expiration policies for projects created before GitLab 12.8 may be enabled by an
admin in the [CI/CD Package Registry settings](./../../admin_area/settings/index.md#cicd).
Note the inherant [risks involved](./index.md#use-with-external-container-registries).
It is possible to create a per-project expiration policy, so that you can make sure that
older tags and images are regularly removed from the Container Registry.
......@@ -539,6 +541,15 @@ Examples:
See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project).
### Use with external container registries
When using an [external container registry](./../../../administration/packages/container_registry.md#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint),
running an experation policy on a project may have some performance risks. If a project is going to run
a policy that will remove large quantities of tags (in the thousands), the GitLab background jobs that
run the policy may get backed up or fail completely. It is recommended you only enable container expiration
policies for projects that were created before GitLab 12.8 if you are confident the amount of tags
being cleaned up will be minimal.
## Limitations
Moving or renaming existing Container Registry repositories is not supported
......
......@@ -110,7 +110,7 @@ module API
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
{
repository: repository.gitaly_repository.to_h,
repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(container.repository_storage),
token: Gitlab::GitalyClient.token(container.repository_storage),
features: Feature::Gitaly.server_feature_flags
......
......@@ -16,6 +16,13 @@ module API
namespace 'internal' do
namespace 'pages' do
desc 'Indicates that pages API is enabled and auth token is valid' do
detail 'This feature was introduced in GitLab 12.10.'
end
get "status" do
no_content!
end
desc 'Get GitLab Pages domain configuration by hostname' do
detail 'This feature was introduced in GitLab 12.3.'
end
......
......@@ -1320,12 +1320,36 @@ msgstr ""
msgid "Admin notes"
msgstr ""
msgid "AdminArea|Active users"
msgstr ""
msgid "AdminArea|Billable users"
msgstr ""
msgid "AdminArea|Blocked users"
msgstr ""
msgid "AdminArea|Bots"
msgstr ""
msgid "AdminArea|Developer"
msgstr ""
msgid "AdminArea|Guest"
msgstr ""
msgid "AdminArea|Included Free in license"
msgstr ""
msgid "AdminArea|Maintainer"
msgstr ""
msgid "AdminArea|Owner"
msgstr ""
msgid "AdminArea|Reporter"
msgstr ""
msgid "AdminArea|Stop all jobs"
msgstr ""
......@@ -1338,15 +1362,18 @@ msgstr ""
msgid "AdminArea|Stopping jobs failed"
msgstr ""
msgid "AdminArea|Users statistics"
msgid "AdminArea|Total users"
msgstr ""
msgid "AdminArea|Users total"
msgid "AdminArea|Users statistics"
msgstr ""
msgid "AdminArea|Users with highest role"
msgstr ""
msgid "AdminArea|Users without a Group and Project"
msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr ""
......@@ -7538,6 +7565,9 @@ msgstr ""
msgid "Enable classification control using an external service"
msgstr ""
msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
msgstr ""
msgid "Enable email restrictions for sign ups"
msgstr ""
......@@ -7934,6 +7964,9 @@ msgstr ""
msgid "Environments|Stopping"
msgstr ""
msgid "Environments|There was an error fetching the logs. Please try again."
msgstr ""
msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?"
msgstr ""
......@@ -8357,6 +8390,9 @@ msgstr ""
msgid "Existing members and groups"
msgstr ""
msgid "Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project."
msgstr ""
msgid "Existing shares"
msgstr ""
......@@ -10650,9 +10686,6 @@ msgstr ""
msgid "IDE|Live Preview"
msgstr ""
msgid "IDE|Open in file view"
msgstr ""
msgid "IDE|Preview your web application using Web IDE client-side evaluation."
msgstr ""
......@@ -12808,9 +12841,6 @@ msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
msgid "Metrics|There was an error fetching the logs, please try again"
msgstr ""
msgid "Metrics|There was an error getting deployment information."
msgstr ""
......@@ -13906,9 +13936,6 @@ msgstr ""
msgid "Open in Xcode"
msgstr ""
msgid "Open in file view"
msgstr ""
msgid "Open issues"
msgstr ""
......
......@@ -2,5 +2,13 @@
FactoryBot.define do
factory :users_statistics do
without_groups_and_projects { 23 }
with_highest_role_guest { 5 }
with_highest_role_reporter { 9 }
with_highest_role_developer { 21 }
with_highest_role_maintainer { 6 }
with_highest_role_owner { 5 }
bots { 2 }
blocked { 7 }
end
end
......@@ -2,14 +2,14 @@
require 'spec_helper'
describe 'admin visits dashboard', :js do
describe 'admin visits dashboard' do
include ProjectForksHelper
before do
sign_in(create(:admin))
end
context 'counting forks' do
context 'counting forks', :js do
it 'correctly counts 2 forks of a project' do
project = create(:project)
project_fork = fork_project(project)
......@@ -25,4 +25,26 @@ describe 'admin visits dashboard', :js do
expect(page).to have_content('Forks 2')
end
end
describe 'Users statistic' do
let_it_be(:users_statistics) { create(:users_statistics) }
it 'shows correct amounts of users', :aggregate_failures do
expected_active_users_text = Gitlab.ee? ? 'Active users (Billable users) 71' : 'Active users 71'
sign_in(create(:admin))
visit admin_dashboard_stats_path
expect(page).to have_content('Users without a Group and Project 23')
expect(page).to have_content('Users with highest role Guest 5')
expect(page).to have_content('Users with highest role Reporter 9')
expect(page).to have_content('Users with highest role Developer 21')
expect(page).to have_content('Users with highest role Maintainer 6')
expect(page).to have_content('Users with highest role Owner 5')
expect(page).to have_content('Bots 2')
expect(page).to have_content(expected_active_users_text)
expect(page).to have_content('Blocked users 7')
expect(page).to have_content('Total users 78')
end
end
end
......@@ -174,7 +174,7 @@ describe 'Set up Mattermost slash commands', :js do
describe 'stable logo url' do
it 'shows a publicly available logo' do
expect(File.exist?(Rails.root.join('public/slash-command-logo.png')))
expect(File.exist?(Rails.root.join('public/slash-command-logo.png'))).to be_truthy
end
end
end
......@@ -47,7 +47,7 @@ describe('EnvironmentLogs', () => {
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' });
const findLogTrace = () => wrapper.find('.js-log-trace');
const findLogTrace = () => wrapper.find({ ref: 'logTrace' });
const findLogFooter = () => wrapper.find({ ref: 'logFooter' });
const getInfiniteScrollAttr = attr => parseInt(findInfiniteScroll().attributes(attr), 10);
......@@ -169,16 +169,12 @@ describe('EnvironmentLogs', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
it('shows an infinite scroll with height and no content', () => {
expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0);
it('shows an infinite scroll with no content', () => {
expect(getInfiniteScrollAttr('fetched-items')).toBe(0);
});
it('shows an infinite scroll container with equal height and max-height ', () => {
const height = getInfiniteScrollAttr('max-list-height');
expect(height).toEqual(expect.any(Number));
expect(findInfiniteScroll().attributes('style')).toMatch(`height: ${height}px;`);
it('shows an infinite scroll container with no set max-height ', () => {
expect(findInfiniteScroll().attributes('max-list-height')).toBeUndefined();
});
it('shows a logs trace', () => {
......@@ -270,8 +266,7 @@ describe('EnvironmentLogs', () => {
expect(findAdvancedFilters().exists()).toBe(true);
});
it('shows infinite scroll with height and no content', () => {
expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0);
it('shows infinite scroll with content', () => {
expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length);
});
......
......@@ -38,7 +38,7 @@ jest.mock('~/logs/utils');
const mockDefaultRange = {
start: '2020-01-10T18:00:00.000Z',
end: '2020-01-10T10:00:00.000Z',
end: '2020-01-10T19:00:00.000Z',
};
const mockFixedRange = {
start: '2020-01-09T18:06:20.000Z',
......@@ -145,9 +145,6 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_ENVIRONMENTS_DATA_ERROR },
],
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
},
);
});
});
......@@ -186,6 +183,7 @@ describe('Logs Store actions', () => {
it('should commit logs and pod data when there is pod name defined', () => {
state.pods.current = mockPodName;
state.timeRange.current = mockFixedRange;
return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => {
expect(latestGetParams()).toMatchObject({
......@@ -214,22 +212,26 @@ describe('Logs Store actions', () => {
state.search = mockSearch;
state.timeRange.current = 'INVALID_TIME_RANGE';
expectedMutations.splice(1, 0, {
type: types.SHOW_TIME_RANGE_INVALID_WARNING,
});
return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => {
expect(latestGetParams()).toEqual({
pod_name: mockPodName,
search: mockSearch,
});
// Warning about time ranges was issued
expect(flash).toHaveBeenCalledTimes(1);
expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning');
});
});
it('should commit logs and pod data when no pod name defined', () => {
state.timeRange.current = mockDefaultRange;
state.timeRange.current = defaultTimeRange;
return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => {
expect(latestGetParams()).toEqual({});
expect(latestGetParams()).toEqual({
start_time: expect.any(String),
end_time: expect.any(String),
});
});
});
});
......@@ -249,6 +251,7 @@ describe('Logs Store actions', () => {
it('should commit logs and pod data when there is pod name defined', () => {
state.pods.current = mockPodName;
state.timeRange.current = mockFixedRange;
expectedActions = [];
......@@ -293,6 +296,10 @@ describe('Logs Store actions', () => {
state.search = mockSearch;
state.timeRange.current = 'INVALID_TIME_RANGE';
expectedMutations.splice(1, 0, {
type: types.SHOW_TIME_RANGE_INVALID_WARNING,
});
return testAction(
fetchMoreLogsPrepend,
null,
......@@ -304,15 +311,12 @@ describe('Logs Store actions', () => {
pod_name: mockPodName,
search: mockSearch,
});
// Warning about time ranges was issued
expect(flash).toHaveBeenCalledTimes(1);
expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning');
},
);
});
it('should commit logs and pod data when no pod name defined', () => {
state.timeRange.current = mockDefaultRange;
state.timeRange.current = defaultTimeRange;
return testAction(
fetchMoreLogsPrepend,
......@@ -321,7 +325,10 @@ describe('Logs Store actions', () => {
expectedMutations,
expectedActions,
() => {
expect(latestGetParams()).toEqual({});
expect(latestGetParams()).toEqual({
start_time: expect.any(String),
end_time: expect.any(String),
});
},
);
});
......@@ -357,6 +364,7 @@ describe('Logs Store actions', () => {
it('fetchLogs should commit logs and pod errors', () => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
state.timeRange.current = defaultTimeRange;
return testAction(
fetchLogs,
......@@ -377,6 +385,7 @@ describe('Logs Store actions', () => {
it('fetchMoreLogsPrepend should commit logs and pod errors', () => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
state.timeRange.current = defaultTimeRange;
return testAction(
fetchMoreLogsPrepend,
......
......@@ -67,6 +67,7 @@ describe('Logs Store Mutations', () => {
options: [],
isLoading: false,
current: null,
fetchError: true,
});
});
});
......@@ -83,6 +84,7 @@ describe('Logs Store Mutations', () => {
expect(state.logs).toEqual({
lines: [],
cursor: null,
fetchError: false,
isLoading: true,
isComplete: false,
});
......@@ -101,6 +103,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: mockCursor,
isComplete: false,
fetchError: false,
});
});
......@@ -115,6 +118,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: null,
isComplete: true,
fetchError: false,
});
});
});
......@@ -128,6 +132,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: null,
isComplete: false,
fetchError: true,
});
});
});
......@@ -152,6 +157,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: mockCursor,
isComplete: false,
fetchError: false,
});
});
......@@ -171,6 +177,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: mockNextCursor,
isComplete: false,
fetchError: false,
});
});
......@@ -185,6 +192,7 @@ describe('Logs Store Mutations', () => {
isLoading: false,
cursor: null,
isComplete: true,
fetchError: false,
});
});
});
......@@ -194,6 +202,7 @@ describe('Logs Store Mutations', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state);
expect(state.logs.isLoading).toBe(false);
expect(state.logs.fetchError).toBe(true);
});
});
......
......@@ -7,17 +7,16 @@ exports[`Repository table row component renders table row 1`] = `
<td
class="tree-item-file-name cursor-default position-relative"
>
<!---->
<a
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
href="https://test.com"
>
<i
aria-label="file"
class="fa fa-fw mr-1 fa-file-text-o"
role="img"
<file-icon-stub
class="mr-1 position-relative text-secondary"
cssclasses="position-relative file-icon"
filename="test"
size="16"
/>
<span
class="position-relative"
......@@ -60,17 +59,16 @@ exports[`Repository table row component renders table row for path with special
<td
class="tree-item-file-name cursor-default position-relative"
>
<!---->
<a
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
href="https://test.com"
>
<i
aria-label="file"
class="fa fa-fw mr-1 fa-file-text-o"
role="img"
<file-icon-stub
class="mr-1 position-relative text-secondary"
cssclasses="position-relative file-icon"
filename="test"
size="16"
/>
<span
class="position-relative"
......
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { GlBadge, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
import TableRow from '~/repository/components/table/row.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
let vm;
let $router;
......@@ -188,7 +188,8 @@ describe('Repository table row component', () => {
vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } });
return vm.vm.$nextTick().then(() => {
expect(vm.find(Icon).exists()).toBe(true);
expect(vm.find(GlIcon).exists()).toBe(true);
expect(vm.find(GlIcon).props('name')).toBe('lock');
});
});
......@@ -202,6 +203,6 @@ describe('Repository table row component', () => {
loadingPath: 'test',
});
expect(vm.find(GlLoadingIcon).exists()).toBe(true);
expect(vm.find(FileIcon).props('loading')).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
describe('File Icon component', () => {
let wrapper;
......@@ -48,7 +47,7 @@ describe('File Icon component', () => {
});
expect(findIcon().exists()).toBe(false);
expect(wrapper.find(Icon).classes()).toContain('folder-icon');
expect(wrapper.find(GlIcon).classes()).toContain('folder-icon');
});
it('should render a loading icon', () => {
......
import Vue from 'vue';
import externalLink from '~/ide/components/external_link.vue';
import createVueComponent from '../../helpers/vue_mount_component_helper';
import { file } from '../helpers';
describe('ExternalLink', () => {
const activeFile = file();
let vm;
function createComponent() {
const ExternalLink = Vue.extend(externalLink);
activeFile.permalink = 'test';
return createVueComponent(ExternalLink, {
file: activeFile,
});
}
afterEach(() => {
vm.$destroy();
});
it('renders the external link with the correct href', done => {
activeFile.binary = true;
vm = createComponent();
vm.$nextTick(() => {
const openLink = vm.$el.querySelector('a');
expect(openLink.href).toMatch(`/${activeFile.permalink}`);
done();
});
});
});
......@@ -159,7 +159,6 @@ describe('graph component', () => {
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
component.pipeline,
component.pipeline.triggered_by[0],
);
});
......@@ -196,7 +195,6 @@ describe('graph component', () => {
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
component.pipeline,
component.pipeline.triggered[0],
);
});
......
......@@ -34,6 +34,10 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
it { is_expected.to allow_value(true).for(:container_expiration_policies_enable_historic_entries) }
it { is_expected.to allow_value(false).for(:container_expiration_policies_enable_historic_entries) }
it { is_expected.not_to allow_value(nil).for(:container_expiration_policies_enable_historic_entries) }
it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) }
it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
......
......@@ -53,7 +53,7 @@ describe Ci::Group do
it 'calls the status from the object itself' do
expect(jobs.first).to receive(:detailed_status)
expect(subject.detailed_status(double(:user)))
subject.detailed_status(double(:user))
end
end
......
......@@ -526,14 +526,14 @@ describe Ci::Runner do
it 'sets a new last_update value when it is called the first time' do
last_update = runner.ensure_runner_queue_value
expect_value_in_queues.to eq(last_update)
expect(value_in_queues).to eq(last_update)
end
it 'does not change if it is not expired and called again' do
last_update = runner.ensure_runner_queue_value
expect(runner.ensure_runner_queue_value).to eq(last_update)
expect_value_in_queues.to eq(last_update)
expect(value_in_queues).to eq(last_update)
end
context 'updates runner queue after changing editable value' do
......@@ -544,7 +544,7 @@ describe Ci::Runner do
end
it 'sets a new last_update value' do
expect_value_in_queues.not_to eq(last_update)
expect(value_in_queues).not_to eq(last_update)
end
end
......@@ -556,14 +556,14 @@ describe Ci::Runner do
end
it 'has an old last_update value' do
expect_value_in_queues.to eq(last_update)
expect(value_in_queues).to eq(last_update)
end
end
def expect_value_in_queues
def value_in_queues
Gitlab::Redis::SharedState.with do |redis|
runner_queue_key = runner.send(:runner_queue_key)
expect(redis.get(runner_queue_key))
redis.get(runner_queue_key)
end
end
end
......
......@@ -2,7 +2,36 @@
require 'spec_helper'
RSpec.describe UsersStatistics do
describe UsersStatistics do
let(:users_statistics) { build(:users_statistics) }
describe 'scopes' do
describe '.order_created_at_desc' do
it 'returns the entries ordered by created at descending' do
users_statistics1 = create(:users_statistics, created_at: Time.current)
users_statistics2 = create(:users_statistics, created_at: Time.current - 2.days)
users_statistics3 = create(:users_statistics, created_at: Time.current - 5.hours)
expect(described_class.order_created_at_desc).to eq(
[
users_statistics1,
users_statistics3,
users_statistics2
]
)
end
end
end
describe '.latest' do
it 'returns the latest entry' do
create(:users_statistics, created_at: Time.current - 1.day)
users_statistics = create(:users_statistics, created_at: Time.current)
expect(described_class.latest).to eq(users_statistics)
end
end
describe '.create_current_stats!' do
before do
create_list(:user_highest_role, 4)
......@@ -40,4 +69,16 @@ RSpec.describe UsersStatistics do
end
end
end
describe '#active' do
it 'sums users statistics values without the value for blocked' do
expect(users_statistics.active).to eq(71)
end
end
describe '#total' do
it 'sums all users statistics values' do
expect(users_statistics.total).to eq(78)
end
end
end
......@@ -7,7 +7,7 @@ describe Ci::PipelinePresenter do
let(:user) { create(:user) }
let(:current_user) { user }
let(:project) { create(:project) }
let(:project) { create(:project, :test_repo) }
let(:pipeline) { create(:ci_pipeline, project: project) }
subject(:presenter) do
......@@ -87,34 +87,32 @@ describe Ci::PipelinePresenter do
end
describe '#name' do
before do
allow(pipeline).to receive(:merge_request_event_type) { event_type }
end
subject { presenter.name }
context 'when pipeline is detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.all_pipelines.last }
context 'for a detached merge request pipeline' do
let(:event_type) { :detached }
it { is_expected.to eq('Detached merge request pipeline') }
end
context 'when pipeline is merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
let(:pipeline) { merge_request.all_pipelines.last }
context 'for a merged result pipeline' do
let(:event_type) { :merged_result }
it { is_expected.to eq('Merged result pipeline') }
end
context 'when pipeline is merge train pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
before do
allow(pipeline).to receive(:merge_request_event_type) { :merge_train }
end
context 'for a merge train pipeline' do
let(:event_type) { :merge_train }
it { is_expected.to eq('Merge train pipeline') }
end
context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:event_type) { nil }
it { is_expected.to eq('Pipeline') }
end
......@@ -145,8 +143,6 @@ describe Ci::PipelinePresenter do
end
context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when ref exists in the repository' do
before do
allow(pipeline).to receive(:ref_exists?) { true }
......@@ -165,7 +161,7 @@ describe Ci::PipelinePresenter do
end
end
context 'when ref exists in the repository' do
context 'when ref does not exist in the repository' do
before do
allow(pipeline).to receive(:ref_exists?) { false }
end
......@@ -188,12 +184,17 @@ describe Ci::PipelinePresenter do
describe '#all_related_merge_request_text' do
subject { presenter.all_related_merge_request_text }
let(:mr_1) { create(:merge_request) }
let(:mr_2) { create(:merge_request) }
context 'with zero related merge requests (branch pipeline)' do
it { is_expected.to eq('No related merge requests found.') }
end
context 'with one related merge request' do
let!(:mr_1) { create(:merge_request, project: project, source_project: project) }
before do
allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: mr_1.id))
end
it {
is_expected.to eq("1 related merge request: " \
......@@ -202,8 +203,9 @@ describe Ci::PipelinePresenter do
end
context 'with two related merge requests' do
let!(:mr_1) { create(:merge_request, project: project, source_project: project, target_branch: 'staging') }
let!(:mr_2) { create(:merge_request, project: project, source_project: project, target_branch: 'feature') }
before do
allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: [mr_1.id, mr_2.id]))
end
it {
is_expected.to eq("2 related merge requests: " \
......@@ -223,22 +225,25 @@ describe Ci::PipelinePresenter do
end
describe '#all_related_merge_requests' do
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
it 'memoizes the returned relation' do
query_count = ActiveRecord::QueryRecorder.new do
3.times { presenter.send(:all_related_merge_requests).count }
end.count
expect(pipeline).to receive(:all_merge_requests_by_recency).exactly(1).time.and_call_original
2.times { presenter.send(:all_related_merge_requests).count }
end
context 'for a branch pipeline with two open MRs' do
let!(:one) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
let!(:two) { create(:merge_request, source_project: project, source_branch: pipeline.ref, target_branch: 'wip') }
expect(query_count).to eq(2)
it { is_expected.to contain_exactly(one, two) }
end
context 'permissions' do
let!(:merge_request) do
create(:merge_request, project: project, source_project: project)
end
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let(:pipeline) { merge_request.all_pipelines.take }
shared_examples 'private merge requests' do
context 'when not logged in' do
......@@ -315,61 +320,51 @@ describe Ci::PipelinePresenter do
describe '#link_to_merge_request' do
subject { presenter.link_to_merge_request }
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.all_pipelines.last }
context 'with a related merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let(:pipeline) { merge_request.all_pipelines.take }
it 'returns a correct link' do
is_expected
.to include(project_merge_request_path(merge_request.project, merge_request))
it 'returns a correct link' do
is_expected.to include(project_merge_request_path(project, merge_request))
end
end
context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'returns nothing' do
is_expected.to be_nil
end
it { is_expected.to be_nil }
end
end
describe '#link_to_merge_request_source_branch' do
subject { presenter.link_to_merge_request_source_branch }
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.all_pipelines.last }
context 'with a related merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let(:pipeline) { merge_request.all_pipelines.take }
it 'returns a correct link' do
is_expected
.to include(project_commits_path(merge_request.source_project,
merge_request.source_branch))
it 'returns a correct link' do
is_expected.to include(project_commits_path(project, merge_request.source_branch))
end
end
context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'returns nothing' do
is_expected.to be_nil
end
it { is_expected.to be_nil }
end
end
describe '#link_to_merge_request_target_branch' do
subject { presenter.link_to_merge_request_target_branch }
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
let(:pipeline) { merge_request.all_pipelines.last }
context 'with a related merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let(:pipeline) { merge_request.all_pipelines.take }
it 'returns a correct link' do
is_expected
.to include(project_commits_path(merge_request.target_project, merge_request.target_branch))
it 'returns a correct link' do
is_expected.to include(project_commits_path(project, merge_request.target_branch))
end
end
context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'returns nothing' do
is_expected.to be_nil
end
it { is_expected.to be_nil }
end
end
end
......@@ -3,13 +3,36 @@
require 'spec_helper'
describe API::Internal::Pages do
describe "GET /internal/pages" do
let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
let(:auth_headers) do
jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
{ Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
end
let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
before do
allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret)
end
describe "GET /internal/pages/status" do
def query_enabled(headers = {})
get api("/internal/pages/status"), headers: headers
end
before do
allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret)
it 'responds with 401 Unauthorized' do
query_enabled
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'responds with 204 no content' do
query_enabled(auth_headers)
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
describe "GET /internal/pages" do
def query_host(host, headers = {})
get api("/internal/pages"), headers: headers, params: { host: host }
end
......
......@@ -22,19 +22,19 @@ describe Ci::ExpirePipelineCacheService do
end
it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master')
merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
merge_request = create(:merge_request, :with_detached_merge_request_pipeline)
project = merge_request.target_project
merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json"
allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch)
expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path)
subject.execute(pipeline)
subject.execute(merge_request.all_pipelines.last)
end
it 'updates the cached status for a project' do
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline)
.with(pipeline)
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).with(pipeline)
subject.execute(pipeline)
end
......
......@@ -8,10 +8,6 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
let(:ref) { merge_request.source_branch }
let(:pipeline) do
create(:ci_pipeline, ref: ref, project: project, sha: sha)
end
let(:service) do
described_class.new(project, user, commit_message: 'Awesome message')
end
......@@ -19,12 +15,11 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
let(:todo_service) { spy('todo service') }
let(:merge_request) do
create(:merge_request, merge_user: user,
source_branch: 'master',
target_branch: 'feature',
source_project: project,
target_project: project,
state: 'opened')
create(:merge_request, :with_detached_merge_request_pipeline, :opened, merge_user: user)
end
let(:pipeline) do
merge_request.all_pipelines.take
end
before do
......
......@@ -15,7 +15,7 @@ describe Users::DestroyService do
it 'deletes the user' do
user_data = service.execute(user)
expect { user_data['email'].to eq(user.email) }
expect(user_data['email']).to eq(user.email)
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
......
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