Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b54cbe2c73
commit
63fd08e6b4
69 changed files with 1116 additions and 538 deletions
|
@ -39,10 +39,10 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pipelineLayers: {
|
||||
type: Array,
|
||||
computedPipelineInfo: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => [],
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
|
@ -81,7 +81,10 @@ export default {
|
|||
layout() {
|
||||
return this.isStageView
|
||||
? this.pipeline.stages
|
||||
: generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers);
|
||||
: generateColumnsFromLayersListMemoized(
|
||||
this.pipeline,
|
||||
this.computedPipelineInfo.pipelineLayers,
|
||||
);
|
||||
},
|
||||
hasDownstreamPipelines() {
|
||||
return Boolean(this.pipeline?.downstream?.length > 0);
|
||||
|
@ -92,6 +95,9 @@ export default {
|
|||
isStageView() {
|
||||
return this.viewType === STAGE_VIEW;
|
||||
},
|
||||
linksData() {
|
||||
return this.computedPipelineInfo?.linksData ?? null;
|
||||
},
|
||||
metricsConfig() {
|
||||
return {
|
||||
path: this.configPaths.metricsPath,
|
||||
|
@ -188,6 +194,7 @@ export default {
|
|||
:container-id="containerId"
|
||||
:container-measurements="measurements"
|
||||
:highlighted-job="hoveredJobName"
|
||||
:links-data="linksData"
|
||||
:metrics-config="metricsConfig"
|
||||
:show-links="showJobLinks"
|
||||
:view-type="viewType"
|
||||
|
|
|
@ -9,11 +9,11 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
|
|||
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
|
||||
import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
import { reportToSentry, reportMessageToSentry } from '../../utils';
|
||||
import { listByLayers } from '../parsing_utils';
|
||||
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
|
||||
import PipelineGraph from './graph_component.vue';
|
||||
import GraphViewSelector from './graph_view_selector.vue';
|
||||
import {
|
||||
calculatePipelineLayersInfo,
|
||||
getQueryHeaders,
|
||||
serializeLoadErrors,
|
||||
toggleQueryPollingByVisibility,
|
||||
|
@ -51,10 +51,10 @@ export default {
|
|||
return {
|
||||
alertType: null,
|
||||
callouts: [],
|
||||
computedPipelineInfo: null,
|
||||
currentViewType: STAGE_VIEW,
|
||||
canRefetchHeaderPipeline: false,
|
||||
pipeline: null,
|
||||
pipelineLayers: null,
|
||||
showAlert: false,
|
||||
showLinks: false,
|
||||
};
|
||||
|
@ -214,12 +214,16 @@ export default {
|
|||
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
|
||||
},
|
||||
methods: {
|
||||
getPipelineLayers() {
|
||||
if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) {
|
||||
this.pipelineLayers = listByLayers(this.pipeline);
|
||||
getPipelineInfo() {
|
||||
if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) {
|
||||
this.computedPipelineInfo = calculatePipelineLayersInfo(
|
||||
this.pipeline,
|
||||
this.$options.name,
|
||||
this.metricsPath,
|
||||
);
|
||||
}
|
||||
|
||||
return this.pipelineLayers;
|
||||
return this.computedPipelineInfo;
|
||||
},
|
||||
handleTipDismissal() {
|
||||
try {
|
||||
|
@ -288,7 +292,7 @@ export default {
|
|||
v-if="pipeline"
|
||||
:config-paths="configPaths"
|
||||
:pipeline="pipeline"
|
||||
:pipeline-layers="getPipelineLayers()"
|
||||
:computed-pipeline-info="getPipelineInfo()"
|
||||
:show-links="showLinks"
|
||||
:view-type="graphViewType"
|
||||
@error="reportFailure"
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import { LOAD_FAILURE } from '../../constants';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { listByLayers } from '../parsing_utils';
|
||||
import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
|
||||
import LinkedPipeline from './linked_pipeline.vue';
|
||||
import {
|
||||
calculatePipelineLayersInfo,
|
||||
getQueryHeaders,
|
||||
serializeLoadErrors,
|
||||
toggleQueryPollingByVisibility,
|
||||
|
@ -138,7 +138,11 @@ export default {
|
|||
},
|
||||
getPipelineLayers(id) {
|
||||
if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
|
||||
this.pipelineLayers[id] = listByLayers(this.currentPipeline);
|
||||
this.pipelineLayers[id] = calculatePipelineLayersInfo(
|
||||
this.currentPipeline,
|
||||
this.$options.name,
|
||||
this.configPaths.metricsPath,
|
||||
);
|
||||
}
|
||||
|
||||
return this.pipelineLayers[id];
|
||||
|
@ -223,7 +227,7 @@ export default {
|
|||
class="d-inline-block gl-mt-n2"
|
||||
:config-paths="configPaths"
|
||||
:pipeline="currentPipeline"
|
||||
:pipeline-layers="getPipelineLayers(pipeline.id)"
|
||||
:computed-pipeline-info="getPipelineLayers(pipeline.id)"
|
||||
:show-links="showLinks"
|
||||
:is-linked-pipeline="true"
|
||||
:view-type="graphViewType"
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import {
|
||||
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
|
||||
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
|
||||
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
PIPELINES_DETAIL_LINK_DURATION,
|
||||
PIPELINES_DETAIL_LINKS_TOTAL,
|
||||
PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
} from '~/performance/constants';
|
||||
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import { reportPerformance } from '../graph_shared/api';
|
||||
|
||||
export const beginPerfMeasure = () => {
|
||||
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
|
||||
};
|
||||
|
||||
export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => {
|
||||
performanceMarkAndMeasure({
|
||||
mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
|
||||
measures: [
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const duration = window.performance.getEntriesByName(
|
||||
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
)[0]?.duration;
|
||||
|
||||
if (!duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
histograms: [
|
||||
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
|
||||
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
value: numLinks / numGroups,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
reportPerformance(metricsPath, data);
|
||||
});
|
||||
};
|
|
@ -1,7 +1,10 @@
|
|||
import { isEmpty } from 'lodash';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { listByLayers } from '../parsing_utils';
|
||||
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
|
||||
import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
|
||||
|
||||
const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
|
||||
return {
|
||||
|
@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
|
|||
};
|
||||
};
|
||||
|
||||
const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
|
||||
const shouldCollectMetrics = Boolean(metricsPath.length);
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
beginPerfMeasure();
|
||||
}
|
||||
|
||||
let layers = null;
|
||||
|
||||
try {
|
||||
layers = listByLayers(pipeline);
|
||||
|
||||
if (shouldCollectMetrics) {
|
||||
finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath);
|
||||
}
|
||||
} catch (err) {
|
||||
reportToSentry(componentName, err);
|
||||
}
|
||||
|
||||
return layers;
|
||||
};
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
const getQueryHeaders = (etagResource) => {
|
||||
return {
|
||||
|
@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
|
|||
const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
|
||||
|
||||
export {
|
||||
calculatePipelineLayersInfo,
|
||||
getQueryHeaders,
|
||||
serializeGqlErr,
|
||||
serializeLoadErrors,
|
||||
|
|
|
@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
|
|||
* @returns {Array} Links that contain all the information about them
|
||||
*/
|
||||
|
||||
export const generateLinksData = ({ links }, containerID, modifier = '') => {
|
||||
export const generateLinksData = (links, containerID, modifier = '') => {
|
||||
const containerEl = document.getElementById(containerID);
|
||||
|
||||
return links.map((link) => {
|
||||
|
|
|
@ -17,8 +17,8 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
parsedData: {
|
||||
type: Object,
|
||||
linksData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pipelineId: {
|
||||
|
@ -95,7 +95,7 @@ export default {
|
|||
highlightedJobs(jobs) {
|
||||
this.$emit('highlightedJobsChange', jobs);
|
||||
},
|
||||
parsedData() {
|
||||
linksData() {
|
||||
this.calculateLinkData();
|
||||
},
|
||||
viewType() {
|
||||
|
@ -112,7 +112,7 @@ export default {
|
|||
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
|
||||
},
|
||||
mounted() {
|
||||
if (!isEmpty(this.parsedData)) {
|
||||
if (!isEmpty(this.linksData)) {
|
||||
this.calculateLinkData();
|
||||
}
|
||||
},
|
||||
|
@ -122,7 +122,7 @@ export default {
|
|||
},
|
||||
calculateLinkData() {
|
||||
try {
|
||||
this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
|
||||
this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`);
|
||||
} catch (err) {
|
||||
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
|
||||
reportToSentry(this.$options.name, err);
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<script>
|
||||
import { isEmpty } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import {
|
||||
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
|
||||
PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
|
||||
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
PIPELINES_DETAIL_LINK_DURATION,
|
||||
PIPELINES_DETAIL_LINKS_TOTAL,
|
||||
PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
} from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import { memoize } from 'lodash';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import { parseData } from '../parsing_utils';
|
||||
import { reportPerformance } from './api';
|
||||
import LinksInner from './links_inner.vue';
|
||||
|
||||
const parseForLinksBare = (pipeline) => {
|
||||
const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
|
||||
return parseData(arrayOfJobs).links;
|
||||
};
|
||||
|
||||
const parseForLinks = memoize(parseForLinksBare);
|
||||
|
||||
export default {
|
||||
name: 'LinksLayer',
|
||||
components: {
|
||||
|
@ -29,10 +25,10 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
metricsConfig: {
|
||||
type: Object,
|
||||
linksData: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
default: () => [],
|
||||
},
|
||||
showLinks: {
|
||||
type: Boolean,
|
||||
|
@ -40,30 +36,16 @@ export default {
|
|||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alertDismissed: false,
|
||||
parsedData: {},
|
||||
showLinksOverride: false,
|
||||
};
|
||||
},
|
||||
i18n: {
|
||||
showLinksAnyways: __('Show links anyways'),
|
||||
tooManyJobs: __(
|
||||
'This graph has a large number of jobs and showing the links between them may have performance implications.',
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
containerZero() {
|
||||
return !this.containerMeasurements.width || !this.containerMeasurements.height;
|
||||
},
|
||||
numGroups() {
|
||||
return this.pipelineData.reduce((acc, { groups }) => {
|
||||
return acc + Number(groups.length);
|
||||
}, 0);
|
||||
},
|
||||
shouldCollectMetrics() {
|
||||
return this.metricsConfig.collectMetrics && this.metricsConfig.path;
|
||||
getLinksData() {
|
||||
if (this.linksData.length > 0) {
|
||||
return this.linksData;
|
||||
}
|
||||
|
||||
return parseForLinks(this.pipelineData);
|
||||
},
|
||||
showLinkedLayers() {
|
||||
return this.showLinks && !this.containerZero;
|
||||
|
@ -72,77 +54,14 @@ export default {
|
|||
errorCaptured(err, _vm, info) {
|
||||
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
|
||||
},
|
||||
mounted() {
|
||||
if (!isEmpty(this.pipelineData)) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.prepareLinkData();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
beginPerfMeasure() {
|
||||
if (this.shouldCollectMetrics) {
|
||||
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
|
||||
}
|
||||
},
|
||||
finishPerfMeasureAndSend(numLinks) {
|
||||
if (this.shouldCollectMetrics) {
|
||||
performanceMarkAndMeasure({
|
||||
mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
|
||||
measures: [
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const duration = window.performance.getEntriesByName(
|
||||
PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
|
||||
)[0]?.duration;
|
||||
|
||||
if (!duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
histograms: [
|
||||
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
|
||||
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
value: numLinks / this.numGroups,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
reportPerformance(this.metricsConfig.path, data);
|
||||
});
|
||||
},
|
||||
prepareLinkData() {
|
||||
this.beginPerfMeasure();
|
||||
let numLinks;
|
||||
try {
|
||||
const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
|
||||
this.parsedData = parseData(arrayOfJobs);
|
||||
numLinks = this.parsedData.links.length;
|
||||
} catch (err) {
|
||||
reportToSentry(this.$options.name, err);
|
||||
}
|
||||
this.finishPerfMeasureAndSend(numLinks);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<links-inner
|
||||
v-if="showLinkedLayers"
|
||||
:container-measurements="containerMeasurements"
|
||||
:parsed-data="parsedData"
|
||||
:links-data="getLinksData"
|
||||
:pipeline-data="pipelineData"
|
||||
:total-groups="numGroups"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
|
|
|
@ -175,7 +175,7 @@ export const listByLayers = ({ stages }) => {
|
|||
const parsedData = parseData(arrayOfJobs);
|
||||
const dataWithLayers = createSankey()(parsedData);
|
||||
|
||||
return dataWithLayers.nodes.reduce((acc, { layer, name }) => {
|
||||
const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
|
||||
/* sort groups by layer */
|
||||
|
||||
if (!acc[layer]) {
|
||||
|
@ -186,6 +186,12 @@ export const listByLayers = ({ stages }) => {
|
|||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
linksData: parsedData.links,
|
||||
numGroups: arrayOfJobs.length,
|
||||
pipelineLayers,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
|
||||
import { getSelectedFragment } from '~/lib/utils/common_utils';
|
||||
|
@ -10,7 +10,6 @@ import ToolbarButton from './toolbar_button.vue';
|
|||
export default {
|
||||
components: {
|
||||
ToolbarButton,
|
||||
GlIcon,
|
||||
GlPopover,
|
||||
GlButton,
|
||||
},
|
||||
|
@ -46,6 +45,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
tag: '> ',
|
||||
suggestPopoverVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -76,15 +76,27 @@ export default {
|
|||
return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showSuggestPopover() {
|
||||
this.updateSuggestPopoverVisibility();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
|
||||
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
|
||||
|
||||
this.updateSuggestPopoverVisibility();
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
|
||||
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
|
||||
},
|
||||
methods: {
|
||||
async updateSuggestPopoverVisibility() {
|
||||
await this.$nextTick();
|
||||
|
||||
this.suggestPopoverVisible = this.showSuggestPopover && this.canSuggest;
|
||||
},
|
||||
isValid(form) {
|
||||
return (
|
||||
!form ||
|
||||
|
@ -153,127 +165,114 @@ export default {
|
|||
</button>
|
||||
</li>
|
||||
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
|
||||
<div class="d-inline-block">
|
||||
<toolbar-button
|
||||
tag="**"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.bold"
|
||||
icon="bold"
|
||||
/>
|
||||
<toolbar-button
|
||||
tag="_"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.italic"
|
||||
icon="italic"
|
||||
/>
|
||||
<toolbar-button
|
||||
tag="**"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.bold"
|
||||
icon="bold"
|
||||
/>
|
||||
<toolbar-button
|
||||
tag="_"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.italic"
|
||||
icon="italic"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
:tag="tag"
|
||||
:button-title="__('Insert a quote')"
|
||||
icon="quote"
|
||||
@click="handleQuote"
|
||||
/>
|
||||
<template v-if="canSuggest">
|
||||
<toolbar-button
|
||||
ref="suggestButton"
|
||||
:tag="mdSuggestion"
|
||||
:prepend="true"
|
||||
:tag="tag"
|
||||
:button-title="__('Insert a quote')"
|
||||
icon="quote"
|
||||
@click="handleQuote"
|
||||
:button-title="__('Insert suggestion')"
|
||||
:cursor-offset="4"
|
||||
:tag-content="lineContent"
|
||||
icon="doc-code"
|
||||
data-qa-selector="suggestion_button"
|
||||
class="js-suggestion-btn"
|
||||
@click="handleSuggestDismissed"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-inline-block ml-md-2 ml-0">
|
||||
<template v-if="canSuggest">
|
||||
<toolbar-button
|
||||
ref="suggestButton"
|
||||
:tag="mdSuggestion"
|
||||
:prepend="true"
|
||||
:button-title="__('Insert suggestion')"
|
||||
:cursor-offset="4"
|
||||
:tag-content="lineContent"
|
||||
icon="doc-code"
|
||||
data-qa-selector="suggestion_button"
|
||||
class="js-suggestion-btn"
|
||||
@click="handleSuggestDismissed"
|
||||
/>
|
||||
<gl-popover
|
||||
v-if="showSuggestPopover && $refs.suggestButton"
|
||||
:target="$refs.suggestButton"
|
||||
:css-classes="['diff-suggest-popover']"
|
||||
placement="bottom"
|
||||
:show="showSuggestPopover"
|
||||
>
|
||||
<strong>{{ __('New! Suggest changes directly') }}</strong>
|
||||
<p class="mb-2">
|
||||
{{
|
||||
__(
|
||||
'Suggest code changes which can be immediately applied in one click. Try it out!',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<gl-button
|
||||
variant="info"
|
||||
category="primary"
|
||||
size="sm"
|
||||
@click="handleSuggestDismissed"
|
||||
>
|
||||
{{ __('Got it') }}
|
||||
</gl-button>
|
||||
</gl-popover>
|
||||
</template>
|
||||
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
|
||||
<toolbar-button
|
||||
tag="[{text}](url)"
|
||||
tag-select="url"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.link"
|
||||
icon="link"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-inline-block ml-md-2 ml-0">
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="- "
|
||||
:button-title="__('Add a bullet list')"
|
||||
icon="list-bulleted"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="1. "
|
||||
:button-title="__('Add a numbered list')"
|
||||
icon="list-numbered"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="- [ ] "
|
||||
:button-title="__('Add a task list')"
|
||||
icon="list-task"
|
||||
/>
|
||||
<toolbar-button
|
||||
:tag="mdCollapsibleSection"
|
||||
:prepend="true"
|
||||
tag-select="Click to expand"
|
||||
:button-title="__('Add a collapsible section')"
|
||||
icon="details-block"
|
||||
/>
|
||||
<toolbar-button
|
||||
:tag="mdTable"
|
||||
:prepend="true"
|
||||
:button-title="__('Add a table')"
|
||||
icon="table"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-inline-block ml-md-2 ml-0">
|
||||
<button
|
||||
v-gl-tooltip
|
||||
:aria-label="__('Go full screen')"
|
||||
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
|
||||
data-container="body"
|
||||
tabindex="-1"
|
||||
:title="__('Go full screen')"
|
||||
type="button"
|
||||
<gl-popover
|
||||
v-if="suggestPopoverVisible"
|
||||
:target="$refs.suggestButton.$el"
|
||||
:css-classes="['diff-suggest-popover']"
|
||||
placement="bottom"
|
||||
:show="suggestPopoverVisible"
|
||||
>
|
||||
<gl-icon name="maximize" />
|
||||
</button>
|
||||
</div>
|
||||
<strong>{{ __('New! Suggest changes directly') }}</strong>
|
||||
<p class="mb-2">
|
||||
{{
|
||||
__(
|
||||
'Suggest code changes which can be immediately applied in one click. Try it out!',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<gl-button
|
||||
variant="info"
|
||||
category="primary"
|
||||
size="small"
|
||||
@click="handleSuggestDismissed"
|
||||
>
|
||||
{{ __('Got it') }}
|
||||
</gl-button>
|
||||
</gl-popover>
|
||||
</template>
|
||||
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
|
||||
<toolbar-button
|
||||
tag="[{text}](url)"
|
||||
tag-select="url"
|
||||
:button-title="
|
||||
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
|
||||
"
|
||||
:shortcuts="$options.shortcuts.link"
|
||||
icon="link"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="- "
|
||||
:button-title="__('Add a bullet list')"
|
||||
icon="list-bulleted"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="1. "
|
||||
:button-title="__('Add a numbered list')"
|
||||
icon="list-numbered"
|
||||
/>
|
||||
<toolbar-button
|
||||
:prepend="true"
|
||||
tag="- [ ] "
|
||||
:button-title="__('Add a task list')"
|
||||
icon="list-task"
|
||||
/>
|
||||
<toolbar-button
|
||||
:tag="mdCollapsibleSection"
|
||||
:prepend="true"
|
||||
tag-select="Click to expand"
|
||||
:button-title="__('Add a collapsible section')"
|
||||
icon="details-block"
|
||||
/>
|
||||
<toolbar-button
|
||||
:tag="mdTable"
|
||||
:prepend="true"
|
||||
:button-title="__('Add a table')"
|
||||
icon="table"
|
||||
/>
|
||||
<toolbar-button
|
||||
class="js-zen-enter"
|
||||
:prepend="true"
|
||||
:button-title="__('Go full screen')"
|
||||
icon="maximize"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -19,7 +19,8 @@ export default {
|
|||
},
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tagBlock: {
|
||||
type: String,
|
||||
|
@ -71,7 +72,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:data-md-tag="tag"
|
||||
:data-md-cursor-offset="cursorOffset"
|
||||
|
@ -82,11 +83,11 @@ export default {
|
|||
:data-md-shortcuts="shortcutsString"
|
||||
:title="buttonTitle"
|
||||
:aria-label="buttonTitle"
|
||||
:icon="icon"
|
||||
type="button"
|
||||
class="toolbar-btn js-md"
|
||||
category="tertiary"
|
||||
class="js-md"
|
||||
data-container="body"
|
||||
@click="() => $emit('click')"
|
||||
>
|
||||
<gl-icon :name="icon" />
|
||||
</button>
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -17,16 +17,6 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const trackingMixin = Tracking.mixin();
|
||||
const trackingInstance = new Vue({
|
||||
...trackingMixin,
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
this.track = trackingInstance.track;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
@ -98,7 +98,6 @@
|
|||
}
|
||||
|
||||
.note-action-button,
|
||||
.toolbar-btn,
|
||||
.dropdown-toggle-caret {
|
||||
@include transition(color);
|
||||
}
|
||||
|
|
|
@ -131,36 +131,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
float: left;
|
||||
padding: 0 7px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
svg {
|
||||
fill: $blue-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-fullscreen-btn {
|
||||
margin-right: -5px;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.md-suggestion-diff {
|
||||
display: table !important;
|
||||
border: 1px solid $border-color !important;
|
||||
|
|
37
app/finders/error_tracking/errors_finder.rb
Normal file
37
app/finders/error_tracking/errors_finder.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ErrorTracking
|
||||
class ErrorsFinder
|
||||
def initialize(current_user, project, params)
|
||||
@current_user = current_user
|
||||
@project = project
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return ErrorTracking::Error.none unless authorized?
|
||||
|
||||
collection = project.error_tracking_errors
|
||||
collection = by_status(collection)
|
||||
|
||||
# Limit collection until pagination implemented
|
||||
collection.limit(20)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :project, :params
|
||||
|
||||
def by_status(collection)
|
||||
if params[:status].present? && ErrorTracking::Error.statuses.key?(params[:status])
|
||||
collection.for_status(params[:status])
|
||||
else
|
||||
collection
|
||||
end
|
||||
end
|
||||
|
||||
def authorized?
|
||||
Ability.allowed?(current_user, :read_sentry_issue, project)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -250,7 +250,7 @@ module MarkupHelper
|
|||
data = options[:data].merge({ container: 'body' })
|
||||
content_tag :button,
|
||||
type: 'button',
|
||||
class: 'toolbar-btn js-md has-tooltip',
|
||||
class: 'gl-button btn btn-default-tertiary btn-icon js-md has-tooltip',
|
||||
data: data,
|
||||
title: options[:title],
|
||||
aria: { label: options[:title] } do
|
||||
|
|
|
@ -5,10 +5,19 @@ class ErrorTracking::Error < ApplicationRecord
|
|||
|
||||
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
|
||||
|
||||
scope :for_status, -> (status) { where(status: status) }
|
||||
|
||||
validates :project, presence: true
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
validates :actor, presence: true
|
||||
validates :status, presence: true
|
||||
|
||||
enum status: {
|
||||
unresolved: 0,
|
||||
resolved: 1,
|
||||
ignored: 2
|
||||
}
|
||||
|
||||
def self.report_error(name:, description:, actor:, platform:, timestamp:)
|
||||
safe_find_or_create_by(
|
||||
|
@ -20,4 +29,64 @@ class ErrorTracking::Error < ApplicationRecord
|
|||
error.update!(last_seen_at: timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
def title
|
||||
if description.present?
|
||||
"#{name} #{description}"
|
||||
else
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
def title_truncated
|
||||
title.truncate(64)
|
||||
end
|
||||
|
||||
# For compatibility with sentry integration
|
||||
def to_sentry_error
|
||||
Gitlab::ErrorTracking::Error.new(
|
||||
id: id,
|
||||
title: title_truncated,
|
||||
message: description,
|
||||
culprit: actor,
|
||||
first_seen: first_seen_at,
|
||||
last_seen: last_seen_at,
|
||||
status: status,
|
||||
count: events_count
|
||||
)
|
||||
end
|
||||
|
||||
# For compatibility with sentry integration
|
||||
def to_sentry_detailed_error
|
||||
Gitlab::ErrorTracking::DetailedError.new(
|
||||
id: id,
|
||||
title: title_truncated,
|
||||
message: description,
|
||||
culprit: actor,
|
||||
first_seen: first_seen_at.to_s,
|
||||
last_seen: last_seen_at.to_s,
|
||||
count: events_count,
|
||||
user_count: 0, # we don't support user count yet.
|
||||
project_id: project.id,
|
||||
status: status,
|
||||
tags: { level: nil, logger: nil },
|
||||
external_url: external_url,
|
||||
external_base_url: external_base_url
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# For compatibility with sentry integration
|
||||
def external_url
|
||||
Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url(
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
issue_id: id)
|
||||
end
|
||||
|
||||
# For compatibility with sentry integration
|
||||
def external_base_url
|
||||
Gitlab::Routing.url_helpers.root_url
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,4 +8,69 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
|
|||
validates :error, presence: true
|
||||
validates :description, presence: true
|
||||
validates :occurred_at, presence: true
|
||||
|
||||
def stacktrace
|
||||
@stacktrace ||= build_stacktrace
|
||||
end
|
||||
|
||||
# For compatibility with sentry integration
|
||||
def to_sentry_error_event
|
||||
Gitlab::ErrorTracking::ErrorEvent.new(
|
||||
issue_id: error_id,
|
||||
date_received: occurred_at,
|
||||
stack_trace_entries: stacktrace
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_stacktrace
|
||||
raw_stacktrace = find_stacktrace_from_payload
|
||||
|
||||
return [] unless raw_stacktrace
|
||||
|
||||
raw_stacktrace.map do |entry|
|
||||
{
|
||||
'lineNo' => entry['lineno'],
|
||||
'context' => build_stacktrace_context(entry),
|
||||
'filename' => entry['filename'],
|
||||
'function' => entry['function'],
|
||||
'colNo' => 0 # we don't support colNo yet.
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def find_stacktrace_from_payload
|
||||
exception_entry = payload.dig('exception')
|
||||
|
||||
if exception_entry
|
||||
exception_values = exception_entry.dig('values')
|
||||
stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
|
||||
stack_trace_entry&.dig('stacktrace', 'frames')
|
||||
end
|
||||
end
|
||||
|
||||
def build_stacktrace_context(entry)
|
||||
context = []
|
||||
error_line = entry['context_line']
|
||||
error_line_no = entry['lineno']
|
||||
pre_context = entry['pre_context']
|
||||
post_context = entry['post_context']
|
||||
|
||||
context += lines_with_position(pre_context, error_line_no - pre_context.size)
|
||||
context += lines_with_position([error_line], error_line_no)
|
||||
context += lines_with_position(post_context, error_line_no + 1)
|
||||
|
||||
context.reject(&:blank?)
|
||||
end
|
||||
|
||||
def lines_with_position(lines, position)
|
||||
return [] if lines.blank?
|
||||
|
||||
lines.map.with_index do |line, index|
|
||||
next unless line
|
||||
|
||||
[position + index, line]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,12 +31,13 @@ module ErrorTracking
|
|||
validates :api_url, length: { maximum: 255 }, public_url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
|
||||
|
||||
validates :enabled, inclusion: { in: [true, false] }
|
||||
validates :integrated, inclusion: { in: [true, false] }
|
||||
|
||||
validates :api_url, presence: { message: 'is a required field' }, if: :enabled
|
||||
|
||||
validate :validate_api_url_path, if: :enabled
|
||||
|
||||
validates :token, presence: { message: 'is a required field' }, if: :enabled
|
||||
with_options if: :sentry_enabled do
|
||||
validates :api_url, presence: { message: 'is a required field' }
|
||||
validates :token, presence: { message: 'is a required field' }
|
||||
validate :validate_api_url_path
|
||||
end
|
||||
|
||||
attr_encrypted :token,
|
||||
mode: :per_attribute_iv,
|
||||
|
@ -45,6 +46,14 @@ module ErrorTracking
|
|||
|
||||
after_save :clear_reactive_cache!
|
||||
|
||||
def sentry_enabled
|
||||
enabled && !integrated_client?
|
||||
end
|
||||
|
||||
def integrated_client?
|
||||
integrated && ::Feature.enabled?(:integrated_error_tracking, project)
|
||||
end
|
||||
|
||||
def api_url=(value)
|
||||
super
|
||||
clear_memoization(:api_url_slugs)
|
||||
|
@ -79,7 +88,7 @@ module ErrorTracking
|
|||
|
||||
def sentry_client
|
||||
strong_memoize(:sentry_client) do
|
||||
ErrorTracking::SentryClient.new(api_url, token)
|
||||
::ErrorTracking::SentryClient.new(api_url, token)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ class GroupPolicy < BasePolicy
|
|||
|
||||
condition(:dependency_proxy_access_allowed) do
|
||||
if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
|
||||
access_level >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
|
||||
access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
|
||||
else
|
||||
can?(:read_group)
|
||||
end
|
||||
|
@ -240,14 +240,14 @@ class GroupPolicy < BasePolicy
|
|||
enable :read_label
|
||||
end
|
||||
|
||||
def access_level
|
||||
def access_level(for_any_session: false)
|
||||
return GroupMember::NO_ACCESS if @user.nil?
|
||||
return GroupMember::NO_ACCESS unless user_is_user?
|
||||
|
||||
@access_level ||= lookup_access_level!
|
||||
@access_level ||= lookup_access_level!(for_any_session: for_any_session)
|
||||
end
|
||||
|
||||
def lookup_access_level!
|
||||
def lookup_access_level!(for_any_session: false)
|
||||
@subject.max_member_access_for_user(@user)
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ module ErrorTracking
|
|||
private
|
||||
|
||||
def perform
|
||||
response = project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
|
||||
response = find_issue_details(params[:issue_id])
|
||||
|
||||
compose_response(response) do
|
||||
# The gitlab_issue attribute can contain an absolute GitLab url from the Sentry Client
|
||||
|
@ -36,5 +36,29 @@ module ErrorTracking
|
|||
def parse_response(response)
|
||||
{ issue: response[:issue] }
|
||||
end
|
||||
|
||||
def find_issue_details(issue_id)
|
||||
# There are 2 types of the data source for the error tracking feature:
|
||||
#
|
||||
# * When integrated error tracking is enabled, we use the application database
|
||||
# to read and save error tracking data.
|
||||
#
|
||||
# * When integrated error tracking is disabled we call
|
||||
# project_error_tracking_setting method which works with Sentry API.
|
||||
#
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
|
||||
#
|
||||
if project_error_tracking_setting.integrated_client?
|
||||
error = project.error_tracking_errors.find(issue_id)
|
||||
|
||||
# We use the same response format as project_error_tracking_setting
|
||||
# method below for compatibility with existing code.
|
||||
{
|
||||
issue: error.to_sentry_detailed_error
|
||||
}
|
||||
else
|
||||
project_error_tracking_setting.issue_details(issue_id: issue_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ module ErrorTracking
|
|||
private
|
||||
|
||||
def perform
|
||||
response = project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
|
||||
response = find_issue_latest_event(params[:issue_id])
|
||||
|
||||
compose_response(response)
|
||||
end
|
||||
|
@ -13,5 +13,30 @@ module ErrorTracking
|
|||
def parse_response(response)
|
||||
{ latest_event: response[:latest_event] }
|
||||
end
|
||||
|
||||
def find_issue_latest_event(issue_id)
|
||||
# There are 2 types of the data source for the error tracking feature:
|
||||
#
|
||||
# * When integrated error tracking is enabled, we use the application database
|
||||
# to read and save error tracking data.
|
||||
#
|
||||
# * When integrated error tracking is disabled we call
|
||||
# project_error_tracking_setting method which works with Sentry API.
|
||||
#
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
|
||||
#
|
||||
if project_error_tracking_setting.integrated_client?
|
||||
error = project.error_tracking_errors.find(issue_id)
|
||||
event = error.events.last
|
||||
|
||||
# We use the same response format as project_error_tracking_setting
|
||||
# method below for compatibility with existing code.
|
||||
{
|
||||
latest_event: event.to_sentry_error_event
|
||||
}
|
||||
else
|
||||
project_error_tracking_setting.issue_latest_event(issue_id: issue_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,10 +5,12 @@ module ErrorTracking
|
|||
private
|
||||
|
||||
def perform
|
||||
response = project_error_tracking_setting.update_issue(
|
||||
update_opts = {
|
||||
issue_id: params[:issue_id],
|
||||
params: update_params
|
||||
)
|
||||
}
|
||||
|
||||
response = update_issue(update_opts)
|
||||
|
||||
compose_response(response) do
|
||||
project_error_tracking_setting.expire_issues_cache
|
||||
|
@ -69,5 +71,31 @@ module ErrorTracking
|
|||
return error('Error Tracking is not enabled') unless enabled?
|
||||
return error('Access denied', :unauthorized) unless can_update?
|
||||
end
|
||||
|
||||
def update_issue(opts)
|
||||
# There are 2 types of the data source for the error tracking feature:
|
||||
#
|
||||
# * When integrated error tracking is enabled, we use the application database
|
||||
# to read and save error tracking data.
|
||||
#
|
||||
# * When integrated error tracking is disabled we call
|
||||
# project_error_tracking_setting method which works with Sentry API.
|
||||
#
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
|
||||
#
|
||||
if project_error_tracking_setting.integrated_client?
|
||||
error = project.error_tracking_errors.find(opts[:issue_id])
|
||||
error.status = opts[:params][:status]
|
||||
error.save!
|
||||
|
||||
# We use the same response format as project_error_tracking_setting
|
||||
# method below for compatibility with existing code.
|
||||
{
|
||||
updated: true
|
||||
}
|
||||
else
|
||||
project_error_tracking_setting.update_issue(**opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,13 +22,15 @@ module ErrorTracking
|
|||
def perform
|
||||
return invalid_status_error unless valid_status?
|
||||
|
||||
response = project_error_tracking_setting.list_sentry_issues(
|
||||
sentry_opts = {
|
||||
issue_status: issue_status,
|
||||
limit: limit,
|
||||
search_term: params[:search_term].presence,
|
||||
sort: sort,
|
||||
cursor: params[:cursor].presence
|
||||
)
|
||||
}
|
||||
|
||||
response = list_issues(sentry_opts)
|
||||
|
||||
compose_response(response)
|
||||
end
|
||||
|
@ -56,5 +58,36 @@ module ErrorTracking
|
|||
def sort
|
||||
params[:sort] || DEFAULT_SORT
|
||||
end
|
||||
|
||||
def list_issues(opts)
|
||||
# There are 2 types of the data source for the error tracking feature:
|
||||
#
|
||||
# * When integrated error tracking is enabled, we use the application database
|
||||
# to read and save error tracking data.
|
||||
#
|
||||
# * When integrated error tracking is disabled we call
|
||||
# project_error_tracking_setting method which works with Sentry API.
|
||||
#
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
|
||||
#
|
||||
if project_error_tracking_setting.integrated_client?
|
||||
# We are going to support more options in the future.
|
||||
# For now we implement the bare minimum for rendering the list in UI.
|
||||
filter_opts = {
|
||||
status: opts[:issue_status]
|
||||
}
|
||||
|
||||
errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute
|
||||
|
||||
# We use the same response format as project_error_tracking_setting
|
||||
# method below for compatibility with existing code.
|
||||
{
|
||||
issues: errors.map(&:to_sentry_error),
|
||||
pagination: {}
|
||||
}
|
||||
else
|
||||
project_error_tracking_setting.list_sentry_issues(**opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,5 +24,5 @@
|
|||
title: _("Add a collapsible section") })
|
||||
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
|
||||
- if show_fullscreen_button
|
||||
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
|
||||
%button.gl-button.btn.btn-default-tertiary.btn-icon.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
|
||||
= sprite_icon("maximize")
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: dast_meta_tag_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::dynamic analysis
|
||||
default_enabled: false
|
49
db/fixtures/development/31_error_tracking.rb
Normal file
49
db/fixtures/development/31_error_tracking.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Gitlab::Seeder::ErrorTrackingSeeder
|
||||
attr_reader :project
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
end
|
||||
|
||||
def seed
|
||||
parsed_event = Gitlab::Json.parse(read_fixture_file('parsed_event.json'))
|
||||
|
||||
ErrorTracking::CollectErrorService
|
||||
.new(project, nil, event: parsed_event)
|
||||
.execute
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_fixture_file(file)
|
||||
File.read(fixture_path(file))
|
||||
end
|
||||
|
||||
def fixture_path(file)
|
||||
Rails.root.join('spec', 'fixtures', 'error_tracking', file)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Gitlab::Seeder.quiet do
|
||||
admin_user = User.admins.first
|
||||
|
||||
Project.not_mass_generated.visible_to_user(admin_user).sample(1).each do |project|
|
||||
puts "\nActivating integrated error tracking for the '#{project.full_path}' project"
|
||||
|
||||
unless Feature.enabled?(:integrated_error_tracking, project)
|
||||
puts '- enabling feature flag'
|
||||
Feature.enable(:integrated_error_tracking, project)
|
||||
end
|
||||
|
||||
puts '- enabling in settings'
|
||||
project.error_tracking_setting || project.create_error_tracking_setting
|
||||
project.error_tracking_setting.update!(enabled: true, integrated: true)
|
||||
|
||||
puts '- seeding an error'
|
||||
seeder = Gitlab::Seeder::ErrorTrackingSeeder.new(project)
|
||||
seeder.seed
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIntegratedToErrorTrackingSetting < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :project_error_tracking_settings, :integrated, :boolean, null: false, default: false
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :project_error_tracking_settings, :integrated
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStatusToErrorTrackingError < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :error_tracking_errors, :status, :integer, null: false, default: 0, limit: 2
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :error_tracking_errors, :status
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeJobIdConversionToBigintForCiJobArtifacts < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = 'ci_job_artifacts'
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
|
||||
table_name: TABLE_NAME,
|
||||
column_name: 'id',
|
||||
job_arguments: [%w[id job_id], %w[id_convert_to_bigint job_id_convert_to_bigint]]
|
||||
)
|
||||
|
||||
swap
|
||||
end
|
||||
|
||||
def down
|
||||
swap
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def swap
|
||||
# This is to replace the existing "index_ci_job_artifacts_on_expire_at_and_job_id" btree (expire_at, job_id)
|
||||
add_concurrent_index TABLE_NAME, [:expire_at, :job_id_convert_to_bigint], name: 'index_ci_job_artifacts_on_expire_at_and_job_id_bigint'
|
||||
# This is to replace the existing "index_ci_job_artifacts_on_job_id_and_file_type" btree (job_id, file_type)
|
||||
add_concurrent_index TABLE_NAME, [:job_id_convert_to_bigint, :file_type], name: 'index_ci_job_artifacts_on_job_id_and_file_type_bigint', unique: true
|
||||
|
||||
# # Add a FK on `job_id_convert_to_bigint` to `ci_builds(id)`, the old FK (fk_rails_c5137cb2c1)
|
||||
# # is removed below since it won't be dropped automatically.
|
||||
fk_ci_builds_job_id = concurrent_foreign_key_name(TABLE_NAME, :job_id, prefix: 'fk_rails_')
|
||||
fk_ci_builds_job_id_tmp = "#{fk_ci_builds_job_id}_tmp"
|
||||
|
||||
add_concurrent_foreign_key TABLE_NAME, :ci_builds,
|
||||
column: :job_id_convert_to_bigint,
|
||||
name: fk_ci_builds_job_id_tmp,
|
||||
on_delete: :cascade,
|
||||
reverse_lock_order: true
|
||||
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
# We'll need ACCESS EXCLUSIVE lock on the related tables,
|
||||
# lets make sure it can be acquired from the start
|
||||
|
||||
execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
|
||||
|
||||
temp_name = 'job_id_tmp'
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:job_id)} TO #{quote_column_name(temp_name)}"
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:job_id_convert_to_bigint)} TO #{quote_column_name(:job_id)}"
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:job_id_convert_to_bigint)}"
|
||||
|
||||
# We need to update the trigger function in order to make PostgreSQL to
|
||||
# regenerate the execution plan for it. This is to avoid type mismatch errors like
|
||||
# "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
|
||||
function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name([:id, :job_id], [:id_convert_to_bigint, :job_id_convert_to_bigint])
|
||||
execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
|
||||
|
||||
# Swap defaults
|
||||
change_column_default TABLE_NAME, :job_id, nil
|
||||
change_column_default TABLE_NAME, :job_id_convert_to_bigint, 0
|
||||
|
||||
# Rename the rest of the indexes (we already hold an exclusive lock, so no need to use DROP INDEX CONCURRENTLY here
|
||||
execute 'DROP INDEX index_ci_job_artifacts_on_expire_at_and_job_id'
|
||||
rename_index TABLE_NAME, 'index_ci_job_artifacts_on_expire_at_and_job_id_bigint', 'index_ci_job_artifacts_on_expire_at_and_job_id'
|
||||
execute 'DROP INDEX index_ci_job_artifacts_on_job_id_and_file_type'
|
||||
rename_index TABLE_NAME, 'index_ci_job_artifacts_on_job_id_and_file_type_bigint', 'index_ci_job_artifacts_on_job_id_and_file_type'
|
||||
|
||||
# Drop original FK on the old int4 `job_id` (fk_rails_c5137cb2c1)
|
||||
remove_foreign_key TABLE_NAME, name: fk_ci_builds_job_id
|
||||
|
||||
# We swapped the columns but the FK for job_id is still using the temporary name for the job_id_convert_to_bigint column
|
||||
# So we have to also swap the FK name now that we dropped the other one with the same
|
||||
rename_constraint(TABLE_NAME, fk_ci_builds_job_id_tmp, fk_ci_builds_job_id)
|
||||
end
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210726134950
Normal file
1
db/schema_migrations/20210726134950
Normal file
|
@ -0,0 +1 @@
|
|||
d989534193566d90f1d4d61a0a588f3204670b67e049e875011a06b32ffd941a
|
1
db/schema_migrations/20210728110654
Normal file
1
db/schema_migrations/20210728110654
Normal file
|
@ -0,0 +1 @@
|
|||
8c317e202b9fb5fc3733325fd2447f65283c3752fcb314033f5d3b2b28484f71
|
1
db/schema_migrations/20210809143931
Normal file
1
db/schema_migrations/20210809143931
Normal file
|
@ -0,0 +1 @@
|
|||
37cac2c3c5c5c22a34e0a77733c5330a32101090ac47b46260123c3362a9e36f
|
|
@ -10787,7 +10787,7 @@ ALTER SEQUENCE ci_instance_variables_id_seq OWNED BY ci_instance_variables.id;
|
|||
CREATE TABLE ci_job_artifacts (
|
||||
id integer NOT NULL,
|
||||
project_id integer NOT NULL,
|
||||
job_id integer NOT NULL,
|
||||
job_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
|
||||
file_type integer NOT NULL,
|
||||
size bigint,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -10799,7 +10799,7 @@ CREATE TABLE ci_job_artifacts (
|
|||
file_format smallint,
|
||||
file_location smallint,
|
||||
id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
|
||||
job_id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
|
||||
job_id bigint NOT NULL,
|
||||
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
|
||||
);
|
||||
|
||||
|
@ -12922,6 +12922,7 @@ CREATE TABLE error_tracking_errors (
|
|||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
events_count bigint DEFAULT 0 NOT NULL,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_18a758e537 CHECK ((char_length(name) <= 255)),
|
||||
CONSTRAINT check_b5cb4d3888 CHECK ((char_length(actor) <= 255)),
|
||||
CONSTRAINT check_c739788b12 CHECK ((char_length(description) <= 1024)),
|
||||
|
@ -17005,7 +17006,8 @@ CREATE TABLE project_error_tracking_settings (
|
|||
encrypted_token character varying,
|
||||
encrypted_token_iv character varying,
|
||||
project_name character varying,
|
||||
organization_name character varying
|
||||
organization_name character varying,
|
||||
integrated boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE project_export_jobs (
|
||||
|
|
|
@ -135,10 +135,10 @@ This is not applicable yet as multiple indices functionality is not fully implem
|
|||
Currently GitLab can only handle a single version of setting. Any setting/schema changes would require reindexing everything from scratch. Since reindexing can take a long time, this can cause search functionality downtime.
|
||||
|
||||
To avoid downtime, GitLab is working to support multiple indices that
|
||||
can function at the same time. Whenever the schema changes, the admin
|
||||
can function at the same time. Whenever the schema changes, the administrator
|
||||
will be able to create a new index and reindex to it, while searches
|
||||
continue to go to the older, stable index. Any data updates will be
|
||||
forwarded to both indices. Once the new index is ready, an admin can
|
||||
forwarded to both indices. Once the new index is ready, an administrator can
|
||||
mark it active, which will direct all searches to it, and remove the old
|
||||
index.
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ projects that need updating. Those projects can be:
|
|||
- Updated recently: Projects that have a `last_repository_updated_at`
|
||||
timestamp that is more recent than the `last_repository_successful_sync_at`
|
||||
timestamp in the `Geo::ProjectRegistry` model.
|
||||
- Manual: The admin can manually flag a repository to resync in the
|
||||
- Manual: The administrator can manually flag a repository to resync in the
|
||||
[Geo admin panel](../user/admin_area/geo_nodes.md).
|
||||
|
||||
When we fail to fetch a repository on the secondary `RETRIES_BEFORE_REDOWNLOAD`
|
||||
|
|
|
@ -279,7 +279,8 @@ export default {
|
|||
```
|
||||
|
||||
The event data can be provided with a `tracking` object, declared in the `data` function,
|
||||
or as a `computed property`.
|
||||
or as a `computed property`. A `tracking` object is convenient when the default
|
||||
event properties are dynamic or provided at runtime.
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
|
@ -292,6 +293,7 @@ export default {
|
|||
// category: '',
|
||||
// property: '',
|
||||
// value: '',
|
||||
// experiment: '',
|
||||
// extra: {},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -277,7 +277,7 @@ On the Route 53 dashboard, click **Hosted zones** in the left navigation bar:
|
|||
1. Click **Create**.
|
||||
1. If you registered your domain through Route 53, you're done. If you used a different domain registrar, you need to update your DNS records with your domain registrar. You'll need to:
|
||||
1. Click on **Hosted zones** and select the domain you added above.
|
||||
1. You'll see a list of `NS` records. From your domain registrar's admin panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
|
||||
1. You'll see a list of `NS` records. From your domain registrar's administrator panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
|
||||
|
||||
The steps for doing this vary depending on which registrar you use and is beyond the scope of this guide.
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ SSO has the following effects when enabled:
|
|||
even if the project is forked.
|
||||
- For a Git activity, users must be signed-in through SSO before they can push to or
|
||||
pull from a GitLab repository.
|
||||
- Users must be signed-in through SSO before they can pull images using the [Dependency Proxy](../../packages/dependency_proxy/index.md).
|
||||
<!-- Add bullet for API activity when https://gitlab.com/gitlab-org/gitlab/-/issues/9152 is complete -->
|
||||
|
||||
## Providers
|
||||
|
|
|
@ -68,11 +68,6 @@ The requirement to authenticate is a breaking change added in 13.7. An [administ
|
|||
disable it](../../../administration/packages/dependency_proxy.md#disabling-authentication) if it
|
||||
has disrupted your existing Dependency Proxy usage.
|
||||
|
||||
WARNING:
|
||||
If [SSO enforcement](../../group/saml_sso/index.md#sso-enforcement)
|
||||
is enabled for your Group, requests to the dependency proxy will fail. This bug is being tracked in
|
||||
[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/294018).
|
||||
|
||||
Because the Dependency Proxy is storing Docker images in a space associated with your group,
|
||||
you must authenticate against the Dependency Proxy.
|
||||
|
||||
|
@ -91,6 +86,12 @@ You can authenticate using:
|
|||
- A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `read_registry` and `write_registry`.
|
||||
- A [group deploy token](../../../user/project/deploy_tokens/index.md#group-deploy-token) with the scope set to `read_registry` and `write_registry`.
|
||||
|
||||
#### SAML SSO
|
||||
|
||||
When [SSO enforcement](../../group/saml_sso/index.md#sso-enforcement)
|
||||
is enabled, users must be signed-in through SSO before they can pull images through the Dependency
|
||||
Proxy.
|
||||
|
||||
#### Authenticate within CI/CD
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in GitLab 13.7.
|
||||
|
|
|
@ -31,7 +31,7 @@ _Taken from the slides [ClearCase and the journey to Git](https://docplayer.net/
|
|||
|
||||
## Why migrate
|
||||
|
||||
ClearCase can be difficult to manage both from a user and an admin perspective.
|
||||
ClearCase can be difficult to manage both from a user and an administrator perspective.
|
||||
Migrating to Git/GitLab there is:
|
||||
|
||||
- **No licensing costs**, Git is GPL while ClearCase is proprietary.
|
||||
|
|
|
@ -64,5 +64,5 @@ in the abuse report's **Message** field.
|
|||
|
||||
## Managing abuse reports
|
||||
|
||||
Admins are able to view and resolve abuse reports.
|
||||
Administrators are able to view and resolve abuse reports.
|
||||
For more information, see [abuse reports administration documentation](admin_area/review_abuse_reports.md).
|
||||
|
|
|
@ -306,10 +306,10 @@ GitLab instance.
|
|||
## Search settings
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292941) in GitLab 13.8 behind a feature flag, disabled by default.
|
||||
> - [Added to Group, Admin, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9.
|
||||
> - [Added to Group, Administrator, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/294025) in GitLab 13.11.
|
||||
|
||||
You can search inside a Project, Group, Admin, or User's settings by entering
|
||||
You can search inside a Project, Group, Administrator, or User's settings by entering
|
||||
a search term in the search box located at the top of the page. The search results
|
||||
appear highlighted in the sections that match the search term.
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ module Gitlab
|
|||
Config::Yaml::Tags::TagError
|
||||
].freeze
|
||||
|
||||
attr_reader :root, :context, :ref, :source
|
||||
attr_reader :root, :context, :source_ref_path, :source
|
||||
|
||||
def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, ref: nil, source: nil)
|
||||
@context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline)
|
||||
def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, source_ref_path: nil, source: nil)
|
||||
@context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline, ref: source_ref_path)
|
||||
@context.set_deadline(TIMEOUT_SECONDS)
|
||||
|
||||
@ref = ref
|
||||
@source_ref_path = source_ref_path
|
||||
@source = source
|
||||
|
||||
@config = expand_config(config)
|
||||
|
@ -108,13 +108,13 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def build_context(project:, sha:, user:, parent_pipeline:)
|
||||
def build_context(project:, sha:, user:, parent_pipeline:, ref:)
|
||||
Config::External::Context.new(
|
||||
project: project,
|
||||
sha: sha || find_sha(project),
|
||||
user: user,
|
||||
parent_pipeline: parent_pipeline,
|
||||
variables: build_variables(project: project, ref: sha))
|
||||
variables: build_variables(project: project, ref: ref))
|
||||
end
|
||||
|
||||
def build_variables(project:, ref:)
|
||||
|
|
|
@ -14,7 +14,7 @@ module Gitlab
|
|||
result = ::Gitlab::Ci::YamlProcessor.new(
|
||||
@command.config_content, {
|
||||
project: project,
|
||||
ref: @pipeline.ref,
|
||||
source_ref_path: @pipeline.source_ref_path,
|
||||
sha: @pipeline.sha,
|
||||
source: @pipeline.source,
|
||||
user: current_user,
|
||||
|
|
|
@ -294,6 +294,7 @@ excluded_attributes:
|
|||
- :encrypted_token
|
||||
- :encrypted_token_iv
|
||||
- :enabled
|
||||
- :integrated
|
||||
service_desk_setting:
|
||||
- :outgoing_name
|
||||
priorities:
|
||||
|
|
|
@ -8,7 +8,8 @@ module Gitlab
|
|||
@project = project
|
||||
@repository_ref = repository_ref.presence
|
||||
|
||||
super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
|
||||
# use the default filter for project searches since we are already limiting by a single project
|
||||
super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters, default_project_filter: true)
|
||||
end
|
||||
|
||||
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
|
||||
|
|
|
@ -168,7 +168,7 @@ module Gitlab
|
|||
issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
|
||||
|
||||
unless default_project_filter
|
||||
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
|
||||
issues = issues.in_projects(project_ids_relation)
|
||||
end
|
||||
|
||||
apply_sort(issues, scope: 'issues')
|
||||
|
|
|
@ -10438,6 +10438,9 @@ msgstr ""
|
|||
msgid "DastSiteValidation|Copy HTTP header to clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Copy Meta tag to clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Could not create validation token. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -10450,6 +10453,9 @@ msgstr ""
|
|||
msgid "DastSiteValidation|Header validation"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Meta tag validation"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Retry validation"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10462,12 +10468,18 @@ msgstr ""
|
|||
msgid "DastSiteValidation|Step 2 - Add following HTTP header to your site"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Step 2 - Add following meta tag to your site"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Step 2 - Add following text to the target site"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Step 3 - Confirm header location and validate"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Step 3 - Confirm meta tag location and validate"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Step 3 - Confirm text file location and validate"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15892,7 +15904,7 @@ msgstr ""
|
|||
msgid "GroupSAML|Enable SAML authentication for this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Enforce SSO-only authentication for Git activity for this group"
|
||||
msgid "GroupSAML|Enforce SSO-only authentication for Git and Dependency Proxy activity for this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|Enforce SSO-only authentication for web activity for this group"
|
||||
|
@ -30587,9 +30599,6 @@ msgstr ""
|
|||
msgid "Show latest version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show links anyways"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show list"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33930,9 +33939,6 @@ msgstr ""
|
|||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "This graph has a large number of jobs and showing the links between them may have performance implications."
|
||||
msgstr ""
|
||||
|
||||
msgid "This group"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -147,25 +147,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
|
||||
subject { get_manifest }
|
||||
|
||||
shared_examples 'a successful manifest pull' do
|
||||
it 'sends a file' do
|
||||
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns Content-Disposition: attachment', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
|
||||
expect(response.headers['Content-Length']).to eq(manifest.size)
|
||||
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
|
||||
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
|
||||
expect(response.headers['Content-Disposition']).to match(/^attachment/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'feature enabled' do
|
||||
before do
|
||||
enable_dependency_proxy
|
||||
|
@ -272,21 +253,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'a successful blob pull' do
|
||||
it 'sends a file' do
|
||||
expect(controller).to receive(:send_file).with(blob.file.path, {})
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns Content-Disposition: attachment', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Content-Disposition']).to match(/^attachment/)
|
||||
end
|
||||
end
|
||||
|
||||
subject { get_blob }
|
||||
|
||||
context 'feature enabled' do
|
||||
|
|
|
@ -35,5 +35,10 @@ FactoryBot.define do
|
|||
platform { 'ruby' }
|
||||
first_seen_at { Time.now.iso8601 }
|
||||
last_seen_at { Time.now.iso8601 }
|
||||
status { 'unresolved' }
|
||||
|
||||
trait :resolved do
|
||||
status { 'resolved' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,7 +55,7 @@ RSpec.describe 'Project > Tags', :js do
|
|||
note_textarea = page.find('.js-gfm-input')
|
||||
|
||||
# Click on Bold button
|
||||
page.find('.md-header-toolbar button.toolbar-btn:first-child').click
|
||||
page.find('.md-header-toolbar button:first-child').click
|
||||
|
||||
expect(note_textarea.value).to eq('****')
|
||||
end
|
||||
|
|
28
spec/finders/error_tracking/errors_finder_spec.rb
Normal file
28
spec/finders/error_tracking/errors_finder_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ErrorTracking::ErrorsFinder do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { project.creator }
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
let_it_be(:error_resolved) { create(:error_tracking_error, :resolved, project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(user, project, params).execute }
|
||||
|
||||
it { is_expected.to contain_exactly(error, error_resolved) }
|
||||
|
||||
context 'with status parameter' do
|
||||
let(:params) { { status: 'resolved' } }
|
||||
|
||||
it { is_expected.to contain_exactly(error_resolved) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,8 +4,8 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
|||
import JobItem from '~/pipelines/components/graph/job_item.vue';
|
||||
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import { listByLayers } from '~/pipelines/components/parsing_utils';
|
||||
import {
|
||||
generateResponse,
|
||||
mockPipelineResponse,
|
||||
|
@ -150,7 +150,7 @@ describe('graph component', () => {
|
|||
},
|
||||
props: {
|
||||
viewType: LAYER_VIEW,
|
||||
pipelineLayers: listByLayers(defaultProps.pipeline),
|
||||
computedPipelineInfo: calculatePipelineLayersInfo(defaultProps.pipeline, 'layer', ''),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import {
|
||||
PIPELINES_DETAIL_LINK_DURATION,
|
||||
PIPELINES_DETAIL_LINKS_TOTAL,
|
||||
PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
} from '~/performance/constants';
|
||||
import * as perfUtils from '~/performance/utils';
|
||||
import {
|
||||
IID_FAILURE,
|
||||
LAYER_VIEW,
|
||||
|
@ -16,9 +24,11 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
|||
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
|
||||
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
import * as Api from '~/pipelines/components/graph_shared/api';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import * as parsingUtils from '~/pipelines/components/parsing_utils';
|
||||
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
import * as sentryUtils from '~/pipelines/utils';
|
||||
import { mockRunningPipelineHeaderData } from '../mock_data';
|
||||
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
|
||||
|
||||
|
@ -480,4 +490,112 @@ describe('Pipeline graph wrapper', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('performance metrics', () => {
|
||||
const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
|
||||
let markAndMeasure;
|
||||
let reportToSentry;
|
||||
let reportPerformance;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
|
||||
markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
|
||||
reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
|
||||
reportPerformance = jest.spyOn(Api, 'reportPerformance');
|
||||
});
|
||||
|
||||
describe('with no metrics path', () => {
|
||||
beforeEach(async () => {
|
||||
createComponentWithApollo();
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('is not called', () => {
|
||||
expect(markAndMeasure).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metrics path', () => {
|
||||
const duration = 875;
|
||||
const numLinks = 7;
|
||||
const totalGroups = 8;
|
||||
const metricsData = {
|
||||
histograms: [
|
||||
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
|
||||
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
value: numLinks / totalGroups,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('when no duration is obtained', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
createComponentWithApollo({
|
||||
provide: {
|
||||
metricsPath,
|
||||
glFeatures: {
|
||||
pipelineGraphLayersView: true,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
currentViewType: LAYER_VIEW,
|
||||
},
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('attempts to collect metrics', () => {
|
||||
expect(markAndMeasure).toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with duration and no error', () => {
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onPost(metricsPath).reply(200, {});
|
||||
|
||||
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
|
||||
return [{ duration }];
|
||||
});
|
||||
|
||||
createComponentWithApollo({
|
||||
provide: {
|
||||
metricsPath,
|
||||
glFeatures: {
|
||||
pipelineGraphLayersView: true,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
currentViewType: LAYER_VIEW,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('it calls reportPerformance with expected arguments', () => {
|
||||
expect(markAndMeasure).toHaveBeenCalled();
|
||||
expect(reportPerformance).toHaveBeenCalled();
|
||||
expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('Links Inner component', () => {
|
|||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
parsedData: parseData(currentPipelineData.flatMap(({ groups }) => groups)),
|
||||
linksData: parseData(currentPipelineData.flatMap(({ groups }) => groups)).links,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import {
|
||||
PIPELINES_DETAIL_LINK_DURATION,
|
||||
PIPELINES_DETAIL_LINKS_TOTAL,
|
||||
PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
} from '~/performance/constants';
|
||||
import * as perfUtils from '~/performance/utils';
|
||||
import * as Api from '~/pipelines/components/graph_shared/api';
|
||||
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import * as sentryUtils from '~/pipelines/utils';
|
||||
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
|
||||
|
||||
describe('links layer component', () => {
|
||||
|
@ -94,139 +84,4 @@ describe('links layer component', () => {
|
|||
expect(findLinksInner().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performance metrics', () => {
|
||||
const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
|
||||
let markAndMeasure;
|
||||
let reportToSentry;
|
||||
let reportPerformance;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
|
||||
markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
|
||||
reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
|
||||
reportPerformance = jest.spyOn(Api, 'reportPerformance');
|
||||
});
|
||||
|
||||
describe('with no metrics config object', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('is not called', () => {
|
||||
expect(markAndMeasure).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metrics config set to false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
metricsConfig: {
|
||||
collectMetrics: false,
|
||||
metricsPath: '/path/to/metrics',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('is not called', () => {
|
||||
expect(markAndMeasure).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no metrics path', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
metricsConfig: {
|
||||
collectMetrics: true,
|
||||
metricsPath: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('is not called', () => {
|
||||
expect(markAndMeasure).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with metrics path and collect set to true', () => {
|
||||
const duration = 875;
|
||||
const numLinks = 7;
|
||||
const totalGroups = 8;
|
||||
const metricsData = {
|
||||
histograms: [
|
||||
{ name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
|
||||
{ name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
|
||||
{
|
||||
name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
|
||||
value: numLinks / totalGroups,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('when no duration is obtained', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
metricsConfig: {
|
||||
collectMetrics: true,
|
||||
path: metricsPath,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('attempts to collect metrics', () => {
|
||||
expect(markAndMeasure).toHaveBeenCalled();
|
||||
expect(reportPerformance).not.toHaveBeenCalled();
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with duration and no error', () => {
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onPost(metricsPath).reply(200, {});
|
||||
|
||||
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
|
||||
return [{ duration }];
|
||||
});
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
metricsConfig: {
|
||||
collectMetrics: true,
|
||||
path: metricsPath,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('it calls reportPerformance with expected arguments', () => {
|
||||
expect(markAndMeasure).toHaveBeenCalled();
|
||||
expect(reportPerformance).toHaveBeenCalled();
|
||||
expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
|
||||
expect(reportToSentry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -120,8 +120,8 @@ describe('DAG visualization parsing utilities', () => {
|
|||
|
||||
describe('generateColumnsFromLayersList', () => {
|
||||
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
|
||||
const layers = listByLayers(pipeline);
|
||||
const columns = generateColumnsFromLayersListBare(pipeline, layers);
|
||||
const { pipelineLayers } = listByLayers(pipeline);
|
||||
const columns = generateColumnsFromLayersListBare(pipeline, pipelineLayers);
|
||||
|
||||
it('returns stage-like objects with default name, id, and status', () => {
|
||||
columns.forEach((col, idx) => {
|
||||
|
@ -136,7 +136,7 @@ describe('DAG visualization parsing utilities', () => {
|
|||
it('creates groups that match the list created in listByLayers', () => {
|
||||
columns.forEach((col, idx) => {
|
||||
const groupNames = col.groups.map(({ name }) => name);
|
||||
expect(groupNames).toEqual(layers[idx]);
|
||||
expect(groupNames).toEqual(pipelineLayers[idx]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
|
||||
|
||||
|
@ -25,7 +26,7 @@ describe('toolbar_button', () => {
|
|||
});
|
||||
|
||||
const getButtonShortcutsAttr = () => {
|
||||
return wrapper.find('button').attributes('data-md-shortcuts');
|
||||
return wrapper.find(GlButton).attributes('data-md-shortcuts');
|
||||
};
|
||||
|
||||
describe('keyboard shortcuts', () => {
|
||||
|
|
|
@ -107,7 +107,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
|
|||
context 'when ref is protected' do
|
||||
before do
|
||||
allow(project).to receive(:protected_for?).with('master').and_return(true)
|
||||
allow(project).to receive(:protected_for?).with('b83d6e391c22777fca1ed3012fce84f633d7fed0').and_return(true)
|
||||
allow(project).to receive(:protected_for?).with('refs/heads/master').and_return(true)
|
||||
|
||||
dependencies.map(&:perform!)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ErrorTracking::ErrorEvent, type: :model do
|
||||
let_it_be(:event) { create(:error_tracking_error_event) }
|
||||
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:error) }
|
||||
end
|
||||
|
@ -11,4 +13,33 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
|
|||
it { is_expected.to validate_presence_of(:description) }
|
||||
it { is_expected.to validate_presence_of(:occurred_at) }
|
||||
end
|
||||
|
||||
describe '#stacktrace' do
|
||||
it 'generates a correct stacktrace in expected format' do
|
||||
expected_context = [
|
||||
[132, " end\n"],
|
||||
[133, "\n"],
|
||||
[134, " begin\n"],
|
||||
[135, " block.call(work, *extra)\n"],
|
||||
[136, " rescue Exception => e\n"],
|
||||
[137, " STDERR.puts \"Error reached top of thread-pool: #\{e.message\} (#\{e.class\})\"\n"],
|
||||
[138, " end\n"]
|
||||
]
|
||||
|
||||
expected_entry = {
|
||||
'lineNo' => 135,
|
||||
'context' => expected_context,
|
||||
'filename' => 'puma/thread_pool.rb',
|
||||
'function' => 'block in spawn_thread',
|
||||
'colNo' => 0
|
||||
}
|
||||
|
||||
expect(event.stacktrace).to be_kind_of(Array)
|
||||
expect(event.stacktrace.first).to eq(expected_entry)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_sentry_error_event' do
|
||||
it { expect(event.to_sentry_error_event).to be_kind_of(Gitlab::ErrorTracking::ErrorEvent) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ErrorTracking::Error, type: :model do
|
||||
let_it_be(:error) { create(:error_tracking_error) }
|
||||
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to have_many(:events) }
|
||||
|
@ -13,4 +15,16 @@ RSpec.describe ErrorTracking::Error, type: :model do
|
|||
it { is_expected.to validate_presence_of(:description) }
|
||||
it { is_expected.to validate_presence_of(:actor) }
|
||||
end
|
||||
|
||||
describe '#title' do
|
||||
it { expect(error.title).to eq('ActionView::MissingTemplate Missing template posts/edit') }
|
||||
end
|
||||
|
||||
describe '#to_sentry_error' do
|
||||
it { expect(error.to_sentry_error).to be_kind_of(Gitlab::ErrorTracking::Error) }
|
||||
end
|
||||
|
||||
describe '#to_sentry_detailed_error' do
|
||||
it { expect(error.to_sentry_detailed_error).to be_kind_of(Gitlab::ErrorTracking::DetailedError) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,20 +54,22 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
|
|||
valid_api_url = 'http://example.com/api/0/projects/org-slug/proj-slug/'
|
||||
valid_token = 'token'
|
||||
|
||||
where(:enabled, :token, :api_url, :valid?) do
|
||||
true | nil | nil | false
|
||||
true | nil | valid_api_url | false
|
||||
true | valid_token | nil | false
|
||||
true | valid_token | valid_api_url | true
|
||||
false | nil | nil | true
|
||||
false | nil | valid_api_url | true
|
||||
false | valid_token | nil | true
|
||||
false | valid_token | valid_api_url | true
|
||||
where(:enabled, :integrated, :token, :api_url, :valid?) do
|
||||
true | true | nil | nil | true
|
||||
true | false | nil | nil | false
|
||||
true | false | nil | valid_api_url | false
|
||||
true | false | valid_token | nil | false
|
||||
true | false | valid_token | valid_api_url | true
|
||||
false | false | nil | nil | true
|
||||
false | false | nil | valid_api_url | true
|
||||
false | false | valid_token | nil | true
|
||||
false | false | valid_token | valid_api_url | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
subject.enabled = enabled
|
||||
subject.integrated = integrated
|
||||
subject.token = token
|
||||
subject.api_url = api_url
|
||||
end
|
||||
|
@ -472,4 +474,25 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
|
|||
expect(subject.list_sentry_issues(params)).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sentry_enabled' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:enabled, :integrated, :feature_flag, :sentry_enabled) do
|
||||
true | false | false | true
|
||||
true | true | false | true
|
||||
true | true | true | false
|
||||
false | false | false | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
subject.enabled = enabled
|
||||
subject.integrated = integrated
|
||||
stub_feature_flags(integrated_error_tracking: feature_flag)
|
||||
end
|
||||
|
||||
it { expect(subject.sentry_enabled).to eq(sentry_enabled) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,6 +39,21 @@ RSpec.describe ErrorTracking::IssueDetailsService do
|
|||
include_examples 'error tracking service data not ready', :issue_details
|
||||
include_examples 'error tracking service sentry error handling', :issue_details
|
||||
include_examples 'error tracking service http status handling', :issue_details
|
||||
|
||||
context 'integrated error tracking' do
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
|
||||
let(:params) { { issue_id: error.id } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
end
|
||||
|
||||
it 'returns the error in detailed format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'error tracking service unauthorized user'
|
||||
|
|
|
@ -5,7 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe ErrorTracking::IssueLatestEventService do
|
||||
include_context 'sentry error tracking context'
|
||||
|
||||
subject { described_class.new(project, user) }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(project, user, params) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'with authorized user' do
|
||||
|
@ -25,6 +27,22 @@ RSpec.describe ErrorTracking::IssueLatestEventService do
|
|||
include_examples 'error tracking service data not ready', :issue_latest_event
|
||||
include_examples 'error tracking service sentry error handling', :issue_latest_event
|
||||
include_examples 'error tracking service http status handling', :issue_latest_event
|
||||
|
||||
context 'integrated error tracking' do
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
let_it_be(:event) { create(:error_tracking_error_event, error: error) }
|
||||
|
||||
let(:params) { { issue_id: error.id } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
end
|
||||
|
||||
it 'returns the latest event in expected format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'error tracking service unauthorized user'
|
||||
|
|
|
@ -114,6 +114,21 @@ RSpec.describe ErrorTracking::IssueUpdateService do
|
|||
end
|
||||
|
||||
include_examples 'error tracking service sentry error handling', :update_issue
|
||||
|
||||
context 'integrated error tracking' do
|
||||
let(:error) { create(:error_tracking_error, project: project) }
|
||||
let(:arguments) { { issue_id: error.id, status: 'resolved' } }
|
||||
let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
end
|
||||
|
||||
it 'resolves the error and responds with expected format' do
|
||||
expect(update_service.execute).to eq(update_issue_response)
|
||||
expect(error.reload.status).to eq('resolved')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'error tracking service unauthorized user'
|
||||
|
|
|
@ -52,6 +52,20 @@ RSpec.describe ErrorTracking::ListIssuesService do
|
|||
|
||||
include_examples 'error tracking service unauthorized user'
|
||||
include_examples 'error tracking service disabled'
|
||||
|
||||
context 'integrated error tracking' do
|
||||
let_it_be(:error) { create(:error_tracking_error, project: project) }
|
||||
|
||||
before do
|
||||
error_tracking_setting.update!(integrated: true)
|
||||
end
|
||||
|
||||
it 'returns the error in expected format' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(result[:issues].size).to eq(1)
|
||||
expect(result[:issues].first.to_json).to eq(error.to_sentry_error.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#external_url' do
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a successful blob pull' do
|
||||
it 'sends a file' do
|
||||
expect(controller).to receive(:send_file).with(blob.file.path, {})
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns Content-Disposition: attachment', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Content-Disposition']).to match(/^attachment/)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'a successful manifest pull' do
|
||||
it 'sends a file' do
|
||||
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns Content-Disposition: attachment', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
|
||||
expect(response.headers['Content-Length']).to eq(manifest.size)
|
||||
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
|
||||
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
|
||||
expect(response.headers['Content-Disposition']).to match(/^attachment/)
|
||||
end
|
||||
end
|
|
@ -23,6 +23,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
|
|||
end
|
||||
|
||||
click_on_protect
|
||||
wait_for_requests
|
||||
|
||||
expect(ProtectedBranch.count).to eq(1)
|
||||
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
|
||||
|
|
Loading…
Reference in a new issue