Commit 603c7d4c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 120f4aae
......@@ -44,7 +44,6 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
environmentsPath: '/api/:version/projects/:id/environments',
group(groupId, callback) {
......@@ -474,14 +473,6 @@ const Api = {
return axios.get(url);
},
lsifData(projectPath, commitId, paths) {
const url = Api.buildUrl(this.lsifPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':commit_id', commitId);
return axios.get(url, { params: { paths } });
},
environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
......
......@@ -7,7 +7,7 @@ export default {
Popover,
},
computed: {
...mapState(['currentDefinition', 'currentDefinitionPosition']),
...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']),
},
mounted() {
this.blobViewer = document.querySelector('.blob-viewer');
......@@ -39,5 +39,6 @@ export default {
v-if="currentDefinition"
:position="currentDefinitionPosition"
:data="currentDefinition"
:definition-path-prefix="definitionPathPrefix"
/>
</template>
......@@ -14,6 +14,10 @@ export default {
type: Object,
required: true,
},
definitionPathPrefix: {
type: String,
required: true,
},
},
data() {
return {
......@@ -27,6 +31,11 @@ export default {
top: `${this.position.y + this.position.height}px`,
};
},
definitionPath() {
return (
this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}`
);
},
},
watch: {
position: {
......@@ -67,8 +76,8 @@ export default {
{{ hover.value }}
</p>
</div>
<div v-if="data.definition_url" class="popover-body">
<gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default">
<div v-if="definitionPath" class="popover-body">
<gl-button :href="definitionPath" target="_blank" class="w-100" variant="default">
{{ __('Go to definition') }}
</gl-button>
</div>
......
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
......@@ -12,11 +12,10 @@ export default {
fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA);
api
.lsifData(state.projectPath, state.commitId, [state.blobPath])
axios
.get(state.codeNavUrl)
.then(({ data }) => {
const dataForPath = data[state.blobPath];
const normalizedData = dataForPath.reduce((acc, d) => {
const normalizedData = data.reduce((acc, d) => {
if (d.hover) {
acc[`${d.start_line}:${d.start_char}`] = d;
addInteractionClass(d);
......
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) {
state.projectPath = projectPath;
state.commitId = commitId;
state.blobPath = blobPath;
[types.SET_INITIAL_DATA](state, { codeNavUrl, definitionPathPrefix }) {
state.codeNavUrl = codeNavUrl;
state.definitionPathPrefix = definitionPathPrefix;
},
[types.REQUEST_DATA](state) {
state.loading = true;
......
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
......@@ -39,7 +39,7 @@ export default {
loadBranches() {
this.fetchBranches({ search: this.search });
},
searchBranches: _.debounce(function debounceSearch() {
searchBranches: debounce(function debounceSearch() {
this.loadBranches();
}, 250),
focusSearch() {
......
<script>
import _ from 'underscore';
import { escape as esc } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
......@@ -22,7 +22,7 @@ export default {
commitToCurrentBranchText() {
return sprintf(
s__('IDE|Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
{ branchName: `<strong class="monospace">${esc(this.currentBranchId)}</strong>` },
false,
);
},
......
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { throttle } from 'lodash';
import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
......@@ -53,7 +53,7 @@ export default {
this.$refs.buildTrace.scrollTo(0, 0);
}
},
scrollBuildLog: _.throttle(function buildLogScrollDebounce() {
scrollBuildLog: throttle(function buildLogScrollDebounce() {
const { scrollTop } = this.$refs.buildTrace;
const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
......
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -59,7 +59,7 @@ export default {
loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search });
},
searchMergeRequests: _.debounce(function debounceSearch() {
searchMergeRequests: debounce(function debounceSearch() {
this.loadMergeRequests();
}, 250),
onSearchFocus() {
......
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import ResizablePanel from '../resizable_panel.vue';
......@@ -55,7 +54,7 @@ export default {
return this.extensionTabs.filter(tab => tab.show);
},
tabViews() {
return _.flatten(this.tabs.map(tab => tab.views));
return this.tabs.map(tab => tab.views).flat();
},
aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name));
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { escape as esc } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
......@@ -35,7 +35,7 @@ export default {
return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
{
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
linkStart: `<a href="${esc(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
},
false,
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { isEmpty } from 'lodash';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import { GlLoadingIcon } from '@gitlab/ui';
......@@ -78,7 +78,7 @@ export default {
.then(() => this.initPreview());
},
beforeDestroy() {
if (!_.isEmpty(this.manager)) {
if (!isEmpty(this.manager)) {
this.manager.listener();
}
this.manager = {};
......@@ -125,7 +125,7 @@ export default {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
if (_.isEmpty(this.manager)) {
if (isEmpty(this.manager)) {
this.initPreview();
return;
......
import Vue from 'vue';
import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate';
import { identity } from 'lodash';
import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
......@@ -31,7 +31,7 @@ Vue.use(Translate);
export function initIde(el, options = {}) {
if (!el) return null;
const { rootComponent = ide, extendStore = _.identity } = options;
const { rootComponent = ide, extendStore = identity } = options;
return new Vue({
el,
......
import { Range } from 'monaco-editor';
import { throttle } from 'underscore';
import { throttle } from 'lodash';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
......
import _ from 'underscore';
import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores';
import DecorationsController from './decorations/controller';
......@@ -38,7 +38,7 @@ export default class Editor {
setupThemes();
this.debouncedUpdate = _.debounce(() => {
this.debouncedUpdate = debounce(() => {
this.updateDimensions();
}, 200);
}
......
import $ from 'jquery';
import Vue from 'vue';
import _ from 'underscore';
import { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
......@@ -296,7 +296,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
sprintf(
__('Branch not loaded - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
......
import _ from 'underscore';
import { escape as esc } from 'lodash';
import flash from '~/flash';
import { __, sprintf } from '~/locale';
import service from '../../services';
......@@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
text: sprintf(
__("Branch %{branchName} was not found in this project's repository."),
{
branchName: `<strong>${_.escape(branchId)}</strong>`,
branchName: `<strong>${esc(branchId)}</strong>`,
},
false,
),
......@@ -154,7 +154,7 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
sprintf(
__('An error occurred while getting files for - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
......
import _ from 'underscore';
import { defer } from 'lodash';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
......@@ -71,7 +71,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
// Defer setting the directory data because this triggers some intense rendering.
// The entries is all we need to load the file editor.
_.defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
resolve();
})
......
......@@ -7,17 +7,15 @@ import {
GlAlert,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import LogSimpleFilters from './log_simple_filters.vue';
import LogAdvancedFilters from './log_advanced_filters.vue';
import LogControlButtons from './log_control_buttons.vue';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils';
......@@ -28,12 +26,10 @@ export default {
GlAlert,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll,
DateTimePicker,
LogSimpleFilters,
LogAdvancedFilters,
LogControlButtons,
},
filters: {
......@@ -63,49 +59,22 @@ export default {
traceHeight: 600,
data() {
return {
searchQuery: '',
timeRanges,
isElasticStackCalloutDismissed: false,
scrollDownButtonDisabled: true,
};
},
computed: {
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace']),
timeRangeModel: {
get() {
return this.timeRange.selected;
},
set(val) {
this.setTimeRange(val);
},
},
...mapGetters('environmentLogs', ['trace', 'showAdvancedFilters']),
showLoader() {
return this.logs.isLoading;
},
advancedFeaturesEnabled() {
const environment = this.environments.options.find(
({ name }) => name === this.environments.current,
);
return environment && environment.enable_advanced_logs_querying;
},
disableAdvancedControls() {
return this.environments.isLoading || !this.advancedFeaturesEnabled;
},
shouldShowElasticStackCallout() {
return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls;
},
podDropdownText() {
if (this.pods.current) {
return this.pods.current;
} else if (this.advancedFeaturesEnabled) {
// "All pods" is a valid option when advanced querying is available
return s__('Environments|All pods');
}
return s__('Environments|No pod selected');
return (
!this.isElasticStackCalloutDismissed &&
(this.environments.isLoading || !this.showAdvancedFilters)
);
},
},
mounted() {
......@@ -121,7 +90,6 @@ export default {
...mapActions('environmentLogs', [
'setInitData',
'setSearch',
'setTimeRange',
'showPodLogs',
'showEnvironment',
'fetchEnvironments',
......@@ -131,9 +99,6 @@ export default {
isCurrentEnvironment(envName) {
return envName === this.environments.current;
},
isCurrentPod(podName) {
return podName === this.pods.current;
},
topReached() {
if (!this.logs.isLoading) {
this.fetchMoreLogsPrepend();
......@@ -167,123 +132,49 @@ export default {
</strong>
</a>
</gl-alert>
<div class="top-bar js-top-bar d-flex">
<div class="row mx-n1">
<gl-form-group
id="environments-dropdown-fg"
label-size="sm"
label-for="environments-dropdown"
class="col-3 px-1"
<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
id="environments-dropdown"
:text="environments.current"
:disabled="environments.isLoading"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown"
>
<gl-dropdown
id="environments-dropdown"
:text="environments.current"
:disabled="environments.isLoading"
class="d-flex gl-h-32 js-environments-dropdown"
toggle-class="dropdown-menu-toggle"
<gl-dropdown-header class="text-center">
{{ s__('Environments|Select environment') }}
</gl-dropdown-header>
<gl-dropdown-item
v-for="env in environments.options"
:key="env.id"
@click="showEnvironment(env.name)"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Select environment') }}
</gl-dropdown-header>
<gl-dropdown-item
v-for="env in environments.options"
:key="env.id"
@click="showEnvironment(env.name)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentEnvironment(env.name) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ env.name }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group
id="pods-dropdown-fg"
label-size="sm"
label-for="pods-dropdown"
class="col-3 px-1"
>
<gl-dropdown
id="pods-dropdown"
:text="podDropdownText"
:disabled="environments.isLoading"
class="d-flex gl-h-32 js-pods-dropdown"
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Filter by pod') }}
</gl-dropdown-header>
<template v-if="advancedFeaturesEnabled">
<gl-dropdown-item key="all-pods" @click="showPodLogs(null)">
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(null) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
</div>
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item v-if="!pods.options.length" :disabled="true">
<span class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group id="search-fg" label-size="sm" label-for="search" class="col-3 px-1">
<gl-search-box-by-click
v-model.trim="searchQuery"
:disabled="disableAdvancedControls"
:placeholder="s__('Environments|Search')"
class="js-logs-search"
type="search"
autofocus
@submit="setSearch(searchQuery)"
/>
</gl-form-group>
<gl-form-group
id="dates-fg"
label-size="sm"
label-for="time-window-dropdown"
class="col-3 px-1"
>
<date-time-picker
ref="dateTimePicker"
v-model="timeRangeModel"
class="w-100 gl-h-32"
right
:disabled="disableAdvancedControls"
:options="timeRanges"
/>
</gl-form-group>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentEnvironment(env.name) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ env.name }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
<log-advanced-filters
v-if="showAdvancedFilters"
ref="log-advanced-filters"
class="d-md-flex flex-grow-1"
:disabled="environments.isLoading"
/>
<log-simple-filters
v-else
ref="log-simple-filters"
class="d-md-flex flex-grow-1"
:disabled="environments.isLoading"
/>
<log-control-buttons
ref="scrollButtons"
class="controllers"
class="flex-grow-0 pr-2 mb-2 controllers"
:scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)"
@scrollDown="scrollDown"
......
<script>
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { mapActions, mapState } from 'vuex';
import {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
} from '@gitlab/ui';
import { timeRanges } from '~/vue_shared/constants';
export default {
components: {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
DateTimePicker,
},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
timeRanges,
searchQuery: '',
};
},
computed: {
...mapState('environmentLogs', ['timeRange', 'pods']),
timeRangeModel: {
get() {
return this.timeRange.selected;
},
set(val) {
this.setTimeRange(val);
},
},
podDropdownText() {
return this.pods.current || s__('Environments|All pods');
},
},
methods: {
...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']),
isCurrentPod(podName) {
return podName === this.pods.current;
},
},
};
</script>
<template>
<div>
<gl-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Filter by pod') }}
</gl-dropdown-header>
<gl-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<template v-else>
<gl-dropdown-item ref="allPodsOption" key="all-pods" @click="showPodLogs(null)">
<div class="d-flex">
<gl-icon
:class="{ invisible: pods.current !== null }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
</div>
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
<gl-search-box-by-click
ref="searchBox"
v-model.trim="searchQuery"
:disabled="disabled"
:placeholder="s__('Environments|Search')"
class="mb-2 pr-2 flex-grow-1"
type="search"
autofocus
@submit="setSearch(searchQuery)"
/>
<date-time-picker
ref="dateTimePicker"
v-model="timeRangeModel"
:disabled="disabled"
:options="timeRanges"
class="mb-2 gl-h-32 pr-2 d-block date-time-picker-wrapper"
right
/>
</div>
</template>
<script>
import { s__ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import { GlIcon, GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownItem,
},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
searchQuery: '',
};
},
computed: {
...mapState('environmentLogs', ['pods']),
podDropdownText() {
return this.pods.current || s__('Environments|No pod selected');
},
},
methods: {
...mapActions('environmentLogs', ['showPodLogs']),
isCurrentPod(podName) {
return podName === this.pods.current;
},
},
};
</script>
<template>
<div>
<gl-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Select pod') }}
</gl-dropdown-header>
<gl-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
......@@ -5,5 +5,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = state => state.logs.lines.map(mapTrace).join('\n');
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const showAdvancedFilters = state => {
const environment = state.environments.options.find(
({ name }) => name === state.environments.current,
);
return Boolean(environment?.enable_advanced_logs_querying);
};
......@@ -379,25 +379,19 @@
}
.top-bar {
@include build-trace-top-bar($gl-line-height * 3);
position: relative;
top: 0;
.dropdown-menu-toggle {
width: 200px;
.date-time-picker-wrapper,
.dropdown-toggle {
@include media-breakpoint-up(md) {
width: 140px;
}
@include media-breakpoint-up(sm) {
width: 300px;
@include media-breakpoint-up(lg) {
width: 160px;
}
}
.controllers {
@include build-controllers(16px, flex-end, true, 2);
}
.refresh-control {
@include build-controllers(16px, flex-end, true, 0);
margin-left: 2px;
@include build-controllers(16px, flex-end, false, 2);
}
}
......
......@@ -43,6 +43,7 @@
.border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-color: $border-color; }
.border-bottom-color-default { border-bottom-color: $border-color; }
.border-radius-default { border-radius: $border-radius-default; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
.gl-children-ml-sm-3 > * {
......
......@@ -3,11 +3,10 @@
class Admin::ServicesController < Admin::ApplicationController
include ServiceParams
before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update]
def index
@services = services_templates
@services = Service.find_or_create_templates
end
def edit
......@@ -30,22 +29,9 @@ class Admin::ServicesController < Admin::ApplicationController
private
# rubocop: disable CodeReuse/ActiveRecord
def services_templates
Service.available_services_names.map do |service_name|
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def service
@service ||= Service.find_by(id: params[:id], template: true)
end
# rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42430')
end
end
......@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
def set_code_navigation_build
return if Feature.disabled?(:code_navigation, @project)
artifact =
Ci::JobArtifact
.for_sha(@blob.commit_id, @project.id)
.for_job_name(Ci::Build::CODE_NAVIGATION_JOB_NAME)
.last
@code_navigation_build = artifact&.job
end
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
set_code_navigation_build
render 'show'
end
......
......@@ -26,7 +26,7 @@ module MilestonesHelper
end
end
def milestones_label_path(opts = {})
def milestones_issues_path(opts = {})
if @project
project_issues_path(@project, opts)
elsif @group
......@@ -283,6 +283,27 @@ module MilestonesHelper
can?(current_user, :admin_milestone, @project.group)
end
end
def display_issues_count_warning?(milestone)
milestone_visible_issues_count(milestone) > Milestone::DISPLAY_ISSUES_LIMIT
end
def milestone_issues_count_message(milestone)
total_count = milestone_visible_issues_count(milestone)
limit = Milestone::DISPLAY_ISSUES_LIMIT
link_options = { milestone_title: @milestone.title }
message = _('Showing %{limit} of %{total_count} issues. ') % { limit: limit, total_count: total_count }
message += link_to(_('View all issues'), milestones_issues_path(link_options))
message.html_safe
end
private
def milestone_visible_issues_count(milestone)
@milestone_visible_issues_count ||= milestone.issues_visible_to_user(current_user).size
end
end
MilestonesHelper.prepend_if_ee('EE::MilestonesHelper')
......@@ -33,6 +33,8 @@ module Ci
scheduler_failure: 2
}.freeze
CODE_NAVIGATION_JOB_NAME = 'code_navigation'
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
......
......@@ -31,7 +31,8 @@ module Ci
metrics: 'metrics.txt',
lsif: 'lsif.json',
dotenv: '.env',
cobertura: 'cobertura-coverage.xml'
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json'
}.freeze
INTERNAL_TYPES = {
......@@ -59,7 +60,8 @@ module Ci
dast: :raw,
license_management: :raw,
license_scanning: :raw,
performance: :raw
performance: :raw,
terraform: :raw
}.freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
......@@ -80,6 +82,7 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
......@@ -129,7 +132,8 @@ module Ci
network_referee: 14, ## runner referees
lsif: 15, # LSIF data for code navigation
dotenv: 16,
cobertura: 17
cobertura: 17,
terraform: 18 # Transformed json
}
enum file_format: {
......
# frozen_string_literal: true
module Milestoneish
DISPLAY_ISSUES_LIMIT = 3000
def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count
end
......@@ -55,7 +57,15 @@ module Milestoneish
end
def sorted_issues(user)
issues_visible_to_user(user).preload_associated_models.sort_by_attribute('label_priority')
# This method is used on milestone view to filter opened assigned, opened unassigned and closed issues columns.
# We want a limit of DISPLAY_ISSUES_LIMIT for total issues present on all columns.
limited_ids =
issues_visible_to_user(user).sort_by_attribute('label_priority').limit(DISPLAY_ISSUES_LIMIT)
Issue
.where(id: Issue.select(:id).from(limited_ids))
.preload_associated_models
.sort_by_attribute('label_priority')
end
def sorted_merge_requests(user)
......
......@@ -70,7 +70,7 @@ class Issue < ApplicationRecord
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :preload_associated_models, -> { preload(:labels, project: :namespace) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :public_only, -> { where(confidential: false) }
......
......@@ -46,6 +46,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
scope :by_type, -> (type) { where(type: type) }
scope :templates, -> { where(template: true, type: available_services_types) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
......@@ -259,14 +260,32 @@ class Service < ApplicationRecord
self.category == :issue_tracker
end
# Find all service templates; if some of them do not exist, create them
# within a transaction to perform the lowest possible SQL queries.
def self.find_or_create_templates
create_nonexistent_templates
templates
end
private_class_method def self.create_nonexistent_templates
nonexistent_services = available_services_types - templates.map(&:type)
return if nonexistent_services.empty?
transaction do
nonexistent_services.each do |service_type|
service_type.constantize.create(template: true)
end
end
end
def self.available_services_names
service_names = %w[
alerts
asana
assembla
bamboo
buildkite
bugzilla
buildkite
campfire
custom_issue_tracker
discord
......@@ -278,20 +297,20 @@ class Service < ApplicationRecord
hipchat
irker
jira
mattermost_slash_commands
mattermost
mattermost_slash_commands
microsoft_teams
packagist
pipelines_email
pivotaltracker
prometheus
pushover
redmine
youtrack
slack_slash_commands
slack
slack_slash_commands
teamcity
microsoft_teams
unify_circuit
youtrack
]
if Rails.env.development?
......@@ -301,6 +320,10 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase)
end
def self.available_services_types
available_services_names.map { |service_name| "#{service_name}_service".camelize }
end
def self.build_from_template(project_id, template)
service = template.dup
......
......@@ -26,14 +26,14 @@ module ContentTypeWhitelist
# Here we override and extend CarrierWave's method that does not parse the
# magic headers.
def check_content_type_whitelist!(new_file)
new_file.content_type = mime_magic_content_type(new_file.path)
if content_type_whitelist
content_type = mime_magic_content_type(new_file.path)
if content_type_whitelist && !whitelisted_content_type?(new_file.content_type)
message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
raise CarrierWave::IntegrityError, message
unless whitelisted_content_type?(content_type)
message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
raise CarrierWave::IntegrityError, message
end
end
super(new_file)
end
def whitelisted_content_type?(content_type)
......
......@@ -9,8 +9,9 @@
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
- if native_code_navigation_enabled?(@project)
#js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } }
- if @code_navigation_build
- code_nav_url = raw_project_job_artifacts_url(@project, @code_navigation_build, path: "lsif/#{blob.path}")
#js-code-navigation{ data: { code_nav_url: "#{code_nav_url}.json", definition_path_prefix: project_blob_path(@project, @ref) } }
%article.file-holder
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
- if display_issues_count_warning?(@milestone)
.flash-container
.flash-warning#milestone-issue-count-warning
= milestone_issues_count_message(@milestone)
.row.prepend-top-default
.col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
......
......@@ -3,12 +3,12 @@
- options = { milestone_title: @milestone.title, label_name: label.title }
%li.no-border
= render_label(label, tooltip: false, link: milestones_label_path(options))
= render_label(label, tooltip: false, link: milestones_issues_path(options))
%span.prepend-description-left
= markdown_field(label, :description)
.float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
= link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
= link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
= link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
---
title: Improve logs filters on mobile, simplify kubernetes API logs filters
merge_request: 27484
author:
type: added
---
title: Reduce number of SQL queries for service templates
merge_request: 27396
author:
type: performance
---
title: Optimize ci builds counters in usage data
merge_request: 27770
author:
type: performance
---
title: Leave upload Content-Type unchaged
merge_request: 27864
author:
type: fixed
---
title: Limits issues displayed on milestones
merge_request: 23102
author:
type: performance
---
title: Log Redis call count and duration to log files
merge_request: 27735
author:
type: other
......@@ -28,6 +28,8 @@ def check_changelog_yaml(path)
if yaml["merge_request"].nil? && !helper.security_mr?
message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}"
elsif yaml["merge_request"] != gitlab.mr_json["iid"]
fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
end
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
# YAML could not be parsed, fail the build.
......
# frozen_string_literal: true
class AddIndexOnUserAndCreatedAtToCiBuilds < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build'
def up
add_concurrent_index :ci_builds, [:user_id, :created_at], where: "type = 'Ci::Build'", name: INDEX_NAME
end
def down
remove_concurrent_index :ci_builds, INDEX_NAME
end
end
......@@ -8603,6 +8603,8 @@ CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON public.ci_builds USING b
CREATE INDEX index_ci_builds_on_user_id ON public.ci_builds USING btree (user_id);
CREATE INDEX index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build ON public.ci_builds USING btree (user_id, created_at) WHERE ((type)::text = 'Ci::Build'::text);
CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON public.ci_builds USING btree (project_id, status) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])));
CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id);
......@@ -12746,5 +12748,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200318165448'),
('20200318175008'),
('20200319203901'),
('20200323075043');
('20200323075043'),
('20200323122201');
......@@ -757,10 +757,7 @@ Repositories may be moved from one storage location using the [Repository
API](../../api/projects.html#edit-project):
```shell
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_token>" \
--data "repository_storage=praefect" \
https://example.gitlab.com/api/v4/projects/123
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "repository_storage=praefect" https://example.gitlab.com/api/v4/projects/123
```
## Debugging Praefect
......
......@@ -255,7 +255,10 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4]
## Display status of database migrations
To check the status of migrations, you can use the following rake task:
See the [upgrade documentation](../../update/README.md#checking-for-background-migrations-before-upgrading)
for how to check that migrations are complete when upgrading GitLab.
To check the status of specific migrations, you can use the following rake task:
```shell
sudo gitlab-rake db:migrate:status
......
......@@ -867,43 +867,7 @@ end
## Sidekiq
### Kill a worker's Sidekiq jobs
```ruby
queue = Sidekiq::Queue.new('repository_import')
queue.each { |job| job.delete if <condition>}
```
`<condition>` probably includes references to job arguments, which depend on the type of job in question.
| queue | worker | job args |
| ----- | ------ | -------- |
| repository_import | RepositoryImportWorker | project_id |
| update_merge_requests | UpdateMergeRequestsWorker | project_id, user_id, oldrev, newrev, ref |
**Example:** Delete all UpdateMergeRequestsWorker jobs associated with a merge request on project_id 125,
merging branch `ref/heads/my_branch`.
```ruby
queue = Sidekiq::Queue.new('update_merge_requests')
queue.each { |job| job.delete if job.args[0]==125 and job.args[4]=='ref/heads/my_branch'}
```
**Note:** Running jobs will not be killed. Stop Sidekiq before doing this, to get all matching jobs.
### Enable debug logging of Sidekiq
```ruby
gitlab_rails['env'] = {
'SIDEKIQ_LOG_ARGUMENTS' => "1"
}
```
Then `gitlab-ctl reconfigure; gitlab-ctl restart sidekiq`. The Sidekiq logs will now include additional data for troubleshooting.
### Sidekiq kill signals
See <https://github.com/mperham/sidekiq/wiki/Signals#ttin>.
This content has been moved to the [Troubleshooting Sidekiq docs](./sidekiq.md).
## Redis
......
......@@ -180,6 +180,13 @@ detach
exit
```
## Sidekiq kill signals
TTIN was described above as the signal to print backtraces for logging, however
Sidekiq responds to other signals as well. For example, TSTP and TERM can be used
to gracefully shut Sidekiq down, see
[the Sidekiq Signals docs](https://github.com/mperham/sidekiq/wiki/Signals#ttin).
## Check for blocking queries
Sometimes the speed at which Sidekiq processes jobs can be so fast that it can
......@@ -260,9 +267,34 @@ end
### Remove Sidekiq jobs for given parameters (destructive)
The general method to kill jobs conditionally is the following:
```ruby
queue = Sidekiq::Queue.new('<queue name>')
queue.each { |job| job.delete if <condition>}
```
NOTE: **Note:** This will remove jobs that are queued but not started, running jobs will not be killed. Have a look at the section below for cancelling running jobs.
In the method above, `<queue-name>` is the name of the queue that contains the job(s) you want to delete and `<condition>` will decide which jobs get deleted.
Commonly, `<condition>` references the job arguments, which depend on the type of job in question. To find the arguments for a specific queue, you can have a look at the `perform` function of the related worker file, commonly found at `/app/workers/<queue-name>_worker.rb`.
For example, `repository_import` has `project_id` as the job argument, while `update_merge_requests` has `project_id, user_id, oldrev, newrev, ref`.
NOTE: **Note:** Arguments need to be referenced by their sequence id using `job.args[<id>]` because `job.args` is a list of all arguments provided to the Sidekiq job.
Here are some examples:
```ruby
queue = Sidekiq::Queue.new('update_merge_requests')
# In this example, we want to remove any update_merge_requests jobs
# for the Project with ID 125 and ref `ref/heads/my_branch`
queue.each { |job| job.delete if job.args[0] == 125 and job.args[4] == 'ref/heads/my_branch' }
```
```ruby
# for jobs like this:
# RepositoryImportWorker.new.perform_async(100)
# Cancelling jobs like: `RepositoryImportWorker.new.perform_async(100)`
id_list = [100]
queue = Sidekiq::Queue.new('repository_import')
......
......@@ -150,8 +150,6 @@ For example:
## Structure
### Organize by topic, not by type
Because we want documentation to be a SSOT, we should [organize by topic, not by type](#organize-by-topic-not-by-type).
### Folder structure overview
......@@ -619,6 +617,22 @@ do not use this option until further notice.
## Links
Links are important in GitLab documentation. They allow you to [link instead of summarizing](#link-instead-of-summarize)
to help preserve an [SSoT](#why-a-single-source-of-truth) within GitLab documentation.
We include guidance for links in the following categories:
- How to set up [anchor links](#anchor-links) for headings.
- How to set up [criteria](#basic-link-criteria) for configuring a link.
- What to set up when [linking to a `help`](../documentation/index.md#linking-to-help) page.
- How to set up [links to internal documentation](#links-to-internal-documentation) for cross-references.
- When to use [links requiring permissions](#links-requiring-permissions).
- How to set up a [link to a video](#link-to-video).
- How to [include links with version text](#text-for-documentation-requiring-version-text).
- How to [link to specific lines of code](#link-to-specific-lines-of-code)
### Basic link criteria
- Use inline link Markdown markup `[Text](https://example.com)`.
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
......@@ -688,6 +702,19 @@ Example:
For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/<issue_number>`.
```
### Link to specific lines of code
When linking to specifics lines within a file, link to a commit instead of to the branch.
Lines of code change through time, therefore, linking to a line by using the commit link
ensures the user lands on the line you're referring to.
- **Do:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/11f17c56d8b7f0b752562d78a4298a3a95b5ce66/.gitlab/issue_templates/Feature%20proposal.md#L3)`
- **Don't:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md#L3).`
If that linked expression is no longer in that line of the file due to further commits, you
can still search the file for that query. In this case, update the document to ensure it
links to the most recent version of the file.
## Navigation
To indicate the steps of navigation through the UI:
......@@ -1361,7 +1388,7 @@ on this document. Further explanation is given below.
- Every method must have the REST API request. For example:
```
```plaintext
GET /projects/:id/repository/branches
```
......
......@@ -122,13 +122,15 @@ If using GitLab 12.9 and newer, run:
sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
```
If using GitLab 12.8 and older, run the following using a Rails console:
If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console):
```ruby
puts Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
```
---
**For installations from source**
If using GitLab 12.9 and newer, run:
......@@ -138,13 +140,16 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
```
If using GitLab 12.8 and older, run the following using a Rails console:
If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console):
```ruby
puts Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
```
There is also a [rake task](../administration/raketasks/maintenance.md#display-status-of-database-migrations)
for displaying the status of each database migration.
## Upgrading to a new major version
Major versions are reserved for backwards incompatible changes. We recommend that
......
......@@ -10,10 +10,13 @@ to perform various actions.
All statistics are opt-out. You can enable/disable them in the
**Admin Area > Settings > Metrics and profiling** section **Usage statistics**.
NOTE: **Note:**
## Network configuration
Allow network traffic from your GitLab instance to IP address `104.196.17.203:443`, to send
usage statistics to GitLab Inc.
If your GitLab instance is behind a proxy, set the appropriate [proxy configuration variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
## Version Check **(CORE ONLY)**
If enabled, version check will inform you if a new version is available and the
......
......@@ -506,9 +506,11 @@ To use SAST in an offline environment, you need:
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
meaning the runner may try to pull remote images even if a local copy is available. Set GitLab
Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images.
meaning the runner will try to pull Docker images from the GitLab container registry even if a local
copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
recommend keeping the pull policy setting to `always` as it will better enable updated scanners to
be utilized within your CI/CD pipelines.
### Make GitLab SAST analyzer images available inside your Docker registry
......
......@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
dotenv cobertura].freeze
dotenv cobertura terraform].freeze
attributes ALLOWED_KEYS
......@@ -36,6 +36,7 @@ module Gitlab
validates :lsif, array_of_strings_or_string: true
validates :dotenv, array_of_strings_or_string: true
validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
end
end
......
......@@ -5,30 +5,11 @@ module Gitlab
module GrapeLogging
module Loggers
class PerfLogger < ::GrapeLogging::Loggers::Base
def parameters(_, _)
gitaly_data.merge(rugged_data)
end
def gitaly_data
gitaly_calls = Gitlab::GitalyClient.get_request_count
include ::Gitlab::InstrumentationHelper
return {} if gitaly_calls.zero?
{
gitaly_calls: Gitlab::GitalyClient.get_request_count,
gitaly_duration: Gitlab::GitalyClient.query_time_ms
}
end
def rugged_data
rugged_calls = Gitlab::RuggedInstrumentation.query_count
return {} if rugged_calls.zero?
{
rugged_calls: rugged_calls,
rugged_duration_ms: Gitlab::RuggedInstrumentation.query_time_ms
}
def parameters(_, _)
payload = {}
payload.tap { add_instrumentation_data(payload) }
end
end
end
......
# frozen_string_literal: true
require 'redis'
module Gitlab
module Instrumentation
module RedisInterceptor
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
if ::RequestStore.active?
::Gitlab::Instrumentation::Redis.increment_request_count
::Gitlab::Instrumentation::Redis.add_duration(duration)
::Gitlab::Instrumentation::Redis.add_call_details(duration, args)
end
end
end
class Redis
REDIS_REQUEST_COUNT = :redis_request_count
REDIS_CALL_DURATION = :redis_call_duration
REDIS_CALL_DETAILS = :redis_call_details
def self.get_request_count
::RequestStore[REDIS_REQUEST_COUNT] || 0
end
def self.increment_request_count
::RequestStore[REDIS_REQUEST_COUNT] ||= 0
::RequestStore[REDIS_REQUEST_COUNT] += 1
end
def self.detail_store
::RequestStore[REDIS_CALL_DETAILS] ||= []
end
def self.query_time_ms
(self.query_time * 1000).round(2)
end
def self.query_time
::RequestStore[REDIS_CALL_DURATION] || 0
end
def self.add_duration(duration)
total_time = query_time + duration
::RequestStore[REDIS_CALL_DURATION] = total_time
end
def self.add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
end
end
end
class ::Redis::Client
prepend ::Gitlab::Instrumentation::RedisInterceptor
end
......@@ -4,7 +4,7 @@ module Gitlab
module InstrumentationHelper
extend self
KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms).freeze
KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms redis_calls redis_duration_ms).freeze
def add_instrumentation_data(payload)
gitaly_calls = Gitlab::GitalyClient.get_request_count
......@@ -20,6 +20,13 @@ module Gitlab
payload[:rugged_calls] = rugged_calls
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
end
redis_calls = Gitlab::Instrumentation::Redis.get_request_count
if redis_calls > 0
payload[:redis_calls] = redis_calls
payload[:redis_duration_ms] = Gitlab::Instrumentation::Redis.query_time_ms
end
end
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
......
......@@ -17,7 +17,7 @@ module Peek
end
def detail_store
::Gitlab::SafeRequestStore["#{key}_call_details"] ||= []
::Gitlab::SafeRequestStore["#{key}_call_details".to_sym] ||= []
end
private
......
# frozen_string_literal: true
require 'redis'
module Gitlab
module Peek
module RedisInstrumented
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
add_call_details(duration, args)
end
private
def add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
def detail_store
::Gitlab::SafeRequestStore['redis_call_details'] ||= []
end
end
end
end
module Peek
module Views
class RedisDetailed < DetailedView
......@@ -63,7 +29,3 @@ module Peek
end
end
end
class Redis::Client
prepend Gitlab::Peek::RedisInstrumented
end
......@@ -220,9 +220,6 @@ msgstr ""
msgid "%{authorsName}'s thread"
msgstr ""
msgid "%{buy_now_link_start}Buy now!%{link_end}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
......@@ -446,6 +443,9 @@ msgstr ""
msgid "%{state} epics"
msgstr ""
msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position."
msgstr ""
msgid "%{strong_start}%{branch_count}%{strong_end} Branch"
msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
msgstr[0] ""
......@@ -584,10 +584,12 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more"
msgstr ""
msgid "+%{approvers} more approvers"
msgstr ""
msgid "+%d more"
msgid_plural "+%d more"
msgstr[0] ""
msgstr[1] ""
msgid "+%{extraOptionCount} more"
msgid "+%{approvers} more approvers"
msgstr ""
msgid "+%{tags} more"
......@@ -2427,12 +2429,6 @@ msgstr ""
msgid "Ascending"
msgstr ""
msgid "Ask an admin to upload a new license to ensure uninterrupted service."
msgstr ""
msgid "Ask an admin to upload a new license to restore service."
msgstr ""
msgid "Ask your group maintainer to set up a group Runner."
msgstr ""
......@@ -7825,6 +7821,9 @@ msgstr ""
msgid "Environments|Select environment"
msgstr ""
msgid "Environments|Select pod"
msgstr ""
msgid "Environments|Show all"
msgstr ""
......@@ -8983,9 +8982,6 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}"
msgstr ""
msgid "Forgot your password?"
msgstr ""
......@@ -13412,6 +13408,9 @@ msgstr ""
msgid "No webhooks found, add one in the form above."
msgstr ""
msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription."
msgstr ""
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
......@@ -16210,12 +16209,6 @@ msgstr ""
msgid "Pushes"
msgstr ""
msgid "Pushing code and creation of issues and merge requests has been disabled."
msgstr ""
msgid "Pushing code and creation of issues and merge requests will be disabled on %{disabled_on}."
msgstr ""
msgid "PushoverService|%{user_name} deleted branch \"%{ref}\"."
msgstr ""
......@@ -18227,6 +18220,9 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
msgid "Showing %{limit} of %{total_count} issues. "
msgstr ""
msgid "Showing %{pageSize} of %{total} issues"
msgstr ""
......@@ -20177,6 +20173,9 @@ msgstr ""
msgid "There was an error updating the dashboard, branch named: %{branch} already exists."
msgstr ""
msgid "There was an error updating the stage order. Please try reloading the page."
msgstr ""
msgid "There was an error when reseting email token."
msgstr ""
......@@ -21654,12 +21653,6 @@ msgstr ""
msgid "Upload a certificate for your domain with all intermediates"
msgstr ""
msgid "Upload a new license in the admin area to ensure uninterrupted service."
msgstr ""
msgid "Upload a new license in the admin area to restore service."
msgstr ""
msgid "Upload a private key for your certificate"
msgstr ""
......@@ -22236,6 +22229,9 @@ msgstr ""
msgid "View Documentation"
msgstr ""
msgid "View all issues"
msgstr ""
msgid "View blame prior to this change"
msgstr ""
......@@ -23053,6 +23049,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
......@@ -23272,6 +23271,9 @@ msgstr ""
msgid "YouTube"
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
......@@ -23392,15 +23394,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr ""
msgid "Your license expired on %{expires_at}."
msgstr ""
msgid "Your license is valid from"
msgstr ""
msgid "Your license will expire in %{remaining_days}."
msgstr ""
msgid "Your message here"
msgstr ""
......@@ -23431,10 +23427,13 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
msgid "Your trial license expired on %{expires_at}."
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
msgid "Your trial license will expire in %{remaining_days}."
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
msgid "Zoom meeting added"
......
......@@ -39,6 +39,7 @@ module QA
autoload :MailHog, 'qa/runtime/mail_hog'
autoload :IPAddress, 'qa/runtime/ip_address'
autoload :Search, 'qa/runtime/search'
autoload :Project, 'qa/runtime/project'
autoload :ApplicationSettings, 'qa/runtime/application_settings'
module API
......
# frozen_string_literal: true
module QA
module Runtime
module Project
extend self
extend Support::Api
def create_project(project_name, api_client, project_description = 'default')
project = Resource::Project.fabricate_via_api! do |project|
project.add_name_uuid = false
project.name = project_name
project.description = project_description
project.api_client = api_client
project.visibility = 'public'
end
project
end
def push_file_to_project(target_project, file_name, file_content)
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = target_project
push.file_name = file_name
push.file_content = file_content
end
end
def set_project_visibility(api_client, project_id, visibility)
request = Runtime::API::Request.new(api_client, "/projects/#{project_id}")
response = put request.url, visibility: visibility
response.code.equal?(QA::Support::Api::HTTP_STATUS_OK)
end
end
end
end
......@@ -42,6 +42,22 @@ module QA
end
end
def elasticsearch_on?(api_client)
elasticsearch_state_request = Runtime::API::Request.new(api_client, '/application/settings')
response = get elasticsearch_state_request.url
parse_body(response)[:elasticsearch_search] && parse_body(response)[:elasticsearch_indexing]
end
def disable_elasticsearch(api_client)
disable_elasticsearch_request = Runtime::API::Request.new(api_client, '/application/settings')
put disable_elasticsearch_request.url, elasticsearch_search: false, elasticsearch_indexing: false
end
def create_search_request(api_client, scope, search_term)
Runtime::API::Request.new(api_client, '/search', scope: scope, search: search_term)
end
def find_code(file_name, search_term)
find_target_in_scope('blobs', search_term) do |record|
record[:filename] == file_name && record[:data].include?(search_term)
......
......@@ -10,21 +10,14 @@ describe Admin::ServicesController do
end
describe 'GET #edit' do
let!(:project) { create(:project) }
Service.available_services_names.each do |service_name|
context "#{service_name}" do
let!(:service) do
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
let!(:service) do
create(:jira_service, :template)
end
it 'successfully displays the template' do
get :edit, params: { id: service.id }
it 'successfully displays the template' do
get :edit, params: { id: service.id }
expect(response).to have_gitlab_http_status(:ok)
end
end
expect(response).to have_gitlab_http_status(:ok)
end
end
......
......@@ -8,18 +8,17 @@ describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
describe "GET show" do
def request
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
end
render_views
context 'with file path' do
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
get(:show,
params: {
namespace_id: project.namespace,
project_id: project,
id: id
})
request
end
context "valid branch, valid file" do
......@@ -119,6 +118,32 @@ describe Projects::BlobController do
end
end
end
context 'when there is an artifact with code navigation data' do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: Ci::Build::CODE_NAVIGATION_JOB_NAME) }
let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
let(:id) { 'master/README.md' }
it 'assigns code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to eq(job)
end
context 'when code_navigation feature is disabled' do
before do
stub_feature_flags(code_navigation: false)
end
it 'does not assign code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to be_nil
end
end
end
end
describe 'GET diff' do
......
......@@ -25,6 +25,37 @@ describe "User views milestone" do
expect { visit_milestone }.not_to exceed_query_limit(control)
end
context 'limiting milestone issues' do
before_all do
2.times do
create(:issue, milestone: milestone, project: project)
create(:issue, milestone: milestone, project: project, assignees: [user])
create(:issue, milestone: milestone, project: project, state: :closed)
end
end
context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do
it "limits issues to display and shows warning" do
stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3)
visit(project_milestone_path(project, milestone))
expect(page).to have_selector('.issuable-row', count: 3)
expect(page).to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
expect(page).to have_link('View all issues', href: project_issues_path(project, { milestone_title: milestone.title }))
end
end
context 'when issues on milestone are below DISPLAY_ISSUES_LIMIT' do
it 'does not display warning' do
visit(project_milestone_path(project, milestone))
expect(page).not_to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
expect(page).to have_selector('.issuable-row', count: 6)
end
end
end
private
def visit_milestone
......
......@@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
page.within('.js-environments-dropdown') do
toggle = find(".dropdown-menu-toggle:not([disabled])")
toggle = find(".dropdown-toggle:not([disabled])")
expect(toggle).to have_content(environment.name)
......@@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
page.within('.js-pods-dropdown') do
find(".dropdown-menu-toggle:not([disabled])").click
page.within('.qa-pods-dropdown') do
find(".dropdown-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])")
expect(dropdown_items.size).to eq(1)
......
......@@ -16,6 +16,7 @@ function factory(initialState = {}) {
state: {
...createState(),
...initialState,
definitionPathPrefix: 'https://test.com/blob/master',
},
actions: {
fetchData,
......
import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue';
const DEFINITION_PATH_PREFIX = 'http:/';
const MOCK_CODE_DATA = Object.freeze({
hover: [
{
......@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log',
},
],
definition_url: 'http://test.com',
definition_path: 'test.com',
});
const MOCK_DOCS_DATA = Object.freeze({
......@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log',
},
],
definition_url: 'http://test.com',
definition_path: 'test.com',
});
let wrapper;
function factory(position, data) {
wrapper = shallowMount(Popover, { propsData: { position, data } });
function factory(position, data, definitionPathPrefix) {
wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
}
describe('Code navigation popover component', () => {
......@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => {
});
it('renders popover', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.element).toMatchSnapshot();
});
describe('code output', () => {
it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
......@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => {
describe('documentation output', () => {
it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA);
factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
......
......@@ -27,12 +27,10 @@ describe('Code navigation actions', () => {
describe('fetchData', () => {
let mock;
const state = {
projectPath: 'gitlab-org/gitlab',
commitId: '123',
blobPath: 'index',
};
const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
const codeNavUrl =
'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
const state = { codeNavUrl };
beforeEach(() => {
window.gon = { api_version: '1' };
......@@ -45,20 +43,18 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(apiUrl).replyOnce(200, {
index: [
{
start_line: 0,
start_char: 0,
hover: { value: '123' },
},
{
start_line: 1,
start_char: 0,
hover: null,
},
],
});
mock.onGet(codeNavUrl).replyOnce(200, [
{
start_line: 0,
start_char: 0,
hover: { value: '123' },
},
{
start_line: 1,
start_char: 0,
hover: null,
},
]);
});
it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
......@@ -106,7 +102,7 @@ describe('Code navigation actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onGet(apiUrl).replyOnce(500);
mock.onGet(codeNavUrl).replyOnce(500);
});
it('dispatches requestDataError', done => {
......
......@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => {
describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => {
mutations.SET_INITIAL_DATA(state, {
projectPath: 'test',
commitId: '123',
blobPath: 'index.js',
codeNavUrl: 'https://test.com/builds/1005',
definitionPathPrefix: 'https://test.com/blob/master',
});
expect(state.projectPath).toBe('test');
expect(state.commitId).toBe('123');
expect(state.blobPath).toBe('index.js');
expect(state.codeNavUrl).toBe('https://test.com/builds/1005');
expect(state.definitionPathPrefix).toBe('https://test.com/blob/master');
});
});
......
......@@ -9,6 +9,8 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('IDE branches search list', () => {
let wrapper;
const fetchBranchesMock = jest.fn();
......
import Vue from 'vue';
import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
import { createStore } from '~/logs/stores';
......@@ -13,7 +11,6 @@ import {
mockLogsResult,
mockTrace,
mockPodName,
mockSearch,
mockEnvironmentsEndpoint,
mockDocumentationPath,
} from '../mock_data';
......@@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () =>
);
describe('EnvironmentLogs', () => {
let EnvironmentLogsComponent;
let store;
let dispatch;
let wrapper;
......@@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => {
const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(itm => !itm.attributes('disabled'));
const findSearchBar = () => wrapper.find('.js-logs-search');
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' });
const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' });
const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert');
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
......@@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => {
};
const initWrapper = () => {
wrapper = shallowMount(EnvironmentLogsComponent, {
wrapper = shallowMount(EnvironmentLogs, {
propsData,
store,
stubs: {
......@@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => {
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
EnvironmentLogsComponent = Vue.extend(EnvironmentLogs);
jest.spyOn(store, 'dispatch').mockResolvedValue();
......@@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
// top bar
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
expect(findPodsDropdown().is(GlDropdown)).toBe(true);
expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true);
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().is(GlSearchBoxByClick)).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
expect(findTimeRangePicker().is(DateTimePicker)).toBe(true);
// log trace
expect(findInfiniteScroll().exists()).toBe(true);
expect(findLogTrace().exists()).toBe(true);
});
......@@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => {
expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0);
});
it('displays a disabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBe('true');
expect(findPodsDropdownItems()).toHaveLength(0);
});
it('displays a disabled search bar', () => {
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().attributes('disabled')).toBe('true');
});
it('displays a disabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
it('does not update buttons state', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
......@@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => {
initWrapper();
});
it('displays a disabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
it('displays a disabled search bar', () => {
expect(findSearchBar().attributes('disabled')).toBe('true');
});
it('displays an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(true);
});
it('displays simple filters for kubernetes logs API', () => {
expect(findSimpleFilters().exists()).toBe(true);
expect(findAdvancedFilters().exists()).toBe(false);
});
});
describe('state with data', () => {
......@@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => {
updateControlBtnsMock.mockReset();
});
it('displays an enabled search bar', () => {
expect(findSearchBar().attributes('disabled')).toBeFalsy();
// input a query and click `search`
findSearchBar().vm.$emit('input', mockSearch);
findSearchBar().vm.$emit('submit');
expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, expect.any(Object));
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
});
it('displays an enabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
});
it('does not display an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(false);
});
......@@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => {
const item = items.at(i);
if (item.text() !== mockEnvName) {
expect(item.find(GlIcon).classes()).toContain('invisible');
expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else {
// selected
expect(item.find(GlIcon).classes()).not.toContain('invisible');
expect(item.find(GlIcon).classes('invisible')).toBe(false);
}
});
});
it('populates pods dropdown', () => {
const items = findPodsDropdownItems();
expect(findPodsDropdown().props('text')).toBe(mockPodName);
expect(items.length).toBe(mockPods.length + 1);
expect(items.at(0).text()).toBe('All pods');
mockPods.forEach((pod, i) => {
const item = items.at(i + 1);
expect(item.text()).toBe(pod);
});
it('displays advanced filters for elasticsearch logs API', () => {
expect(findSimpleFilters().exists()).toBe(false);
expect(findAdvancedFilters().exists()).toBe(true);
});
it('shows infinite scroll with height and no content', () => {
......@@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => {
expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length);
});
it('dropdown has one pod selected', () => {
const items = findPodsDropdownItems();
mockPods.forEach((pod, i) => {
const item = items.at(i);
if (item.text() !== mockPodName) {
expect(item.find(GlIcon).classes()).toContain('invisible');
} else {
// selected
expect(item.find(GlIcon).classes()).not.toContain('invisible');
}
});
});
it('populates logs trace', () => {
const trace = findLogTrace();
expect(trace.text().split('\n').length).toBe(mockTrace.length);
......@@ -371,17 +302,6 @@ describe('EnvironmentLogs', () => {
);
});
it('pod name, trace is refreshed', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
items.at(index + 1).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
it('refresh button, trace is refreshed', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
......
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { defaultTimeRange } from '~/vue_shared/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { createStore } from '~/logs/stores';
import { mockPods, mockSearch } from '../mock_data';
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
const module = 'environmentLogs';
describe('LogAdvancedFilters', () => {
let store;
let dispatch;
let wrapper;
let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(item => !item.is('[disabled]'));
const findPodsDropdownItemsSelected = () =>
findPodsDropdownItems()
.filter(item => {
return !item.find(GlIcon).classes('invisible');
})
.at(0);
const findSearchBox = () => wrapper.find({ ref: 'searchBox' });
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const mockStateLoading = () => {
state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = [];
state.pods.current = null;
};
const mockStateWithData = () => {
state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = mockPods;
state.pods.current = null;
};
const initWrapper = (propsData = {}) => {
wrapper = shallowMount(LogAdvancedFilters, {
propsData: {
...propsData,
},
store,
});
};
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
});
afterEach(() => {
store.dispatch.mockReset();
if (wrapper) {
wrapper.destroy();
}
});
it('displays UI elements', () => {
initWrapper();
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true);
expect(findSearchBox().exists()).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
});
describe('disabled state', () => {
beforeEach(() => {
mockStateLoading();
initWrapper({
disabled: true,
});
});
it('displays disabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods');
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
expect(findSearchBox().attributes('disabled')).toBeTruthy();
expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
});
});
describe('when the state is loading', () => {
beforeEach(() => {
mockStateLoading();
initWrapper();
});
it('displays a enabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods');
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findSearchBox().attributes('disabled')).toBeFalsy();
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
});
it('displays an empty pods dropdown', () => {
expect(findPodsNoPodsText().exists()).toBe(true);
expect(findPodsDropdownItems()).toHaveLength(0);
});
});
describe('when the state has data', () => {
beforeEach(() => {
mockStateWithData();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe('All pods');
});
it('displays options in a pods dropdown', () => {
const items = findPodsDropdownItems();
expect(items).toHaveLength(mockPods.length + 1);
});
it('displays "all pods" selected in a pods dropdown', () => {
const selected = findPodsDropdownItemsSelected();
expect(selected.text()).toBe('All pods');
});
it('displays options in date time picker', () => {
const options = findTimeRangePicker().props('options');
expect(options).toEqual(expect.any(Array));
expect(options.length).toBeGreaterThan(0);
});
describe('when the user interacts', () => {
it('clicks on a all options, showPodLogs is dispatched with null', () => {
const items = findPodsDropdownItems();
items.at(0).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
});
it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
items.at(index + 1).vm.$emit('click'); // skip "All pods" option
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
it('clicks on search, a serches is done', () => {
expect(findSearchBox().attributes('disabled')).toBeFalsy();
// input a query and click `search`
findSearchBox().vm.$emit('input', mockSearch);
findSearchBox().vm.$emit('submit');
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
});
it('selects a new time range', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
const mockRange = { start: 'START_DATE', end: 'END_DATE' };
findTimeRangePicker().vm.$emit('input', mockRange);
expect(dispatch).toHaveBeenCalledWith(`${module}/setTimeRange`, mockRange);
});
});
});
});
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/logs/stores';
import { mockPods, mockPodName } from '../mock_data';
import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
const module = 'environmentLogs';
describe('LogSimpleFilters', () => {
let store;
let dispatch;
let wrapper;
let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(item => !item.is('[disabled]'));
const mockPodsLoading = () => {
state.pods.options = [];
state.pods.current = null;
};
const mockPodsLoaded = () => {
state.pods.options = mockPods;
state.pods.current = mockPodName;
};
const initWrapper = (propsData = {}) => {
wrapper = shallowMount(LogSimpleFilters, {
propsData: {
...propsData,
},
store,
});
};
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
});
afterEach(() => {
store.dispatch.mockReset();
if (wrapper) {
wrapper.destroy();
}
});
it('displays UI elements', () => {
initWrapper();
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true);
});
describe('disabled state', () => {
beforeEach(() => {
mockPodsLoading();
initWrapper({
disabled: true,
});
});
it('displays a disabled pods dropdown', () => {
expect(findPodsDropdown().props('text')).toBe('No pod selected');
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
});
});
describe('loading state', () => {
beforeEach(() => {
mockPodsLoading();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe('No pod selected');
});
it('displays an empty pods dropdown', () => {
expect(findPodsNoPodsText().exists()).toBe(true);
expect(findPodsDropdownItems()).toHaveLength(0);
});
});
describe('pods available state', () => {
beforeEach(() => {
mockPodsLoaded();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe(mockPods[0]);
});
it('displays a pods dropdown with items', () => {
expect(findPodsNoPodsText().exists()).toBe(false);
expect(findPodsDropdownItems()).toHaveLength(mockPods.length);
});
it('dropdown has one pod selected', () => {
const items = findPodsDropdownItems();
mockPods.forEach((pod, i) => {
const item = items.at(i);
if (item.text() !== mockPodName) {
expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else {
expect(item.find(GlIcon).classes('invisible')).toBe(false);
}
});
});
it('when the user clicks on a pod, showPodLogs is dispatched', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
items.at(index).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
});
});
import * as getters from '~/logs/stores/getters';
import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
import { mockLogsResult, mockTrace } from '../mock_data';
import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => {
let state;
......@@ -13,7 +13,7 @@ describe('Logs Store getters', () => {
describe('trace', () => {
describe('when state is initialized', () => {
it('returns an empty string', () => {
expect(getters.trace(state)).toEqual('');
expect(trace(state)).toEqual('');
});
});
......@@ -23,7 +23,7 @@ describe('Logs Store getters', () => {
});
it('returns an empty string', () => {
expect(getters.trace(state)).toEqual('');
expect(trace(state)).toEqual('');
});
});
......@@ -33,7 +33,42 @@ describe('Logs Store getters', () => {
});
it('returns an empty string', () => {
expect(getters.trace(state)).toEqual(mockTrace.join('\n'));
expect(trace(state)).toEqual(mockTrace.join('\n'));
});
});
});
describe('showAdvancedFilters', () => {
describe('when no environments are set', () => {
beforeEach(() => {
state.environments.current = mockEnvName;
state.environments.options = [];
});
it('returns false', () => {
expect(showAdvancedFilters(state)).toBe(false);
});
});
describe('when the environment supports filters', () => {
beforeEach(() => {
state.environments.current = mockEnvName;
state.environments.options = mockEnvironments;
});
it('returns true', () => {
expect(showAdvancedFilters(state)).toBe(true);
});
});
describe('when the environment does not support filters', () => {
beforeEach(() => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvironments[1].name;
});
it('returns true', () => {
expect(showAdvancedFilters(state)).toBe(false);
});
});
});
......
......@@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:lsif | 'lsif.json'
:dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
end
with_them do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GrapeLogging::Loggers::PerfLogger do
subject { described_class.new }
describe ".parameters" do
let(:mock_request) { OpenStruct.new(env: {}) }
describe 'when no performance datais are present' do
it 'returns an empty Hash' do
expect(subject.parameters(mock_request, nil)).to eq({})
end
end
describe 'when Redis calls are present', :request_store do
it 'returns a Hash with Redis information' do
Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') }
payload = subject.parameters(mock_request, nil)
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
end
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require 'spec_helper'
require 'rspec-parameterized'
describe Gitlab::InstrumentationHelper do
using RSpec::Parameterized::TableSyntax
describe '.add_instrumentation_data', :request_store do
let(:payload) { {} }
subject { described_class.add_instrumentation_data(payload) }
it 'adds nothing' do
subject
expect(payload).to eq({})
end
context 'when Gitaly calls are made' do
it 'adds Gitaly data and omits Redis data' do
project = create(:project)
RequestStore.clear!
project.repository.exists?
subject
expect(payload[:gitaly_calls]).to eq(1)
expect(payload[:gitaly_duration]).to be >= 0
expect(payload[:redis_calls]).to be_nil
expect(payload[:redis_duration_ms]).to be_nil
end
end
context 'when Redis calls are made' do
it 'adds Redis data and omits Gitaly data' do
Gitlab::Redis::Cache.with { |redis| redis.get('test-instrumentation') }
subject
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
expect(payload[:gitaly_calls]).to be_nil
expect(payload[:gitaly_duration]).to be_nil
end
end
end
describe '.queue_duration_for_job' do
where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
......
......@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
context 'with Gitaly and Rugged calls' do
context 'with Gitaly, Rugged, and Redis calls' do
let(:timing_data) do
{
gitaly_calls: 10,
gitaly_duration: 10000,
rugged_calls: 1,
rugged_duration_ms: 5000
rugged_duration_ms: 5000,
redis_calls: 3,
redis_duration_ms: 1234
}
end
before do
job.merge!(timing_data)
let(:expected_end_payload) do
end_payload.except('args').merge(timing_data)
end
it 'logs with Gitaly and Rugged timing data' do
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload.except('args')).ordered
expect(logger).to receive(:info).with(end_payload.except('args')).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
subject.call(job, 'test_queue') { }
subject.call(job, 'test_queue') do
job.merge!(timing_data)
end
end
end
end
......
......@@ -140,6 +140,18 @@ describe Ci::JobArtifact do
end
end
describe '.for_job_name' do
it 'returns job artifacts for a given job name' do
first_job = create(:ci_build, name: 'first')
second_job = create(:ci_build, name: 'second')
first_artifact = create(:ci_job_artifact, job: first_job)
second_artifact = create(:ci_job_artifact, job: second_job)
expect(described_class.for_job_name(first_job.name)).to eq([first_artifact])
expect(described_class.for_job_name(second_job.name)).to eq([second_artifact])
end
end
describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) }
......
......@@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do
end
describe '#sorted_issues' do
it 'sorts issues by label priority' do
before do
issue.labels << label_1
security_issue_1.labels << label_2
closed_issue_1.labels << label_3
end
it 'sorts issues by label priority' do
issues = milestone.sorted_issues(member)
expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1)
end
it 'limits issue count and keeps the ordering' do
stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 4)
issues = milestone.sorted_issues(member)
# Cannot use issues.count here because it is sorting
# by a virtual column 'highest_priority' and it will break
# the query.
total_issues_count = issues.opened.unassigned.length + issues.opened.assigned.length + issues.closed.length
expect(issues.length).to eq(4)
expect(total_issues_count).to eq(4)
expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1)
end
end
context 'attributes visibility' do
......
......@@ -149,9 +149,58 @@ describe Service do
end
end
describe "Template" do
describe 'template' do
let(:project) { create(:project) }
shared_examples 'retrieves service templates' do
it 'returns the available service templates' do
expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types)
end
end
describe '.find_or_create_templates' do
it 'creates service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names.size)
end
it_behaves_like 'retrieves service templates'
context 'with all existing templates' do
before do
Service.insert_all(
Service.available_services_types.map { |type| { template: true, type: type } }
)
end
it 'does not create service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.by(0)
end
it_behaves_like 'retrieves service templates'
context 'with a previous existing service (Previous) and a new service (Asana)' do
before do
Service.insert(type: 'PreviousService', template: true)
Service.delete_by(type: 'AsanaService', template: true)
end
it_behaves_like 'retrieves service templates'
end
end
context 'with a few existing templates' do
before do
create(:jira_service, :template)
end
it 'creates the rest of the service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names.size)
end
it_behaves_like 'retrieves service templates'
end
end
describe '.build_from_template' do
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
......
......@@ -34,6 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
job_artifacts_terraform
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
......
......@@ -26,3 +26,15 @@ shared_examples 'accepted carrierwave upload' do
expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile))
end
end
# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg')
# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload.
# @param content_type [String] the upload file content type after cache
shared_examples 'upload with content type' do |content_type|
let(:fixture_file) { fixture_file_upload(path, content_type) }
it 'will not change upload file content type' do
uploader.cache!(fixture_file)
expect(uploader.file.content_type).to eq(content_type)
end
end
......@@ -18,6 +18,7 @@ describe ContentTypeWhitelist do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
it_behaves_like 'accepted carrierwave upload'
it_behaves_like 'upload with content type', 'image/jpeg'
end
context 'upload non-whitelisted file content type' do
......
......@@ -97,5 +97,12 @@ describe JobArtifactUploader do
it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
# CI job artifacts usually are shown as text/plain, but they contain
# escape characters so MIME detectors usually fail to determine what
# the Content-Type is.
it 'does not set Content-Type' do
expect(uploader.file.content_type).to be_blank
end
end
end
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
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