Commit 37ff5631 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab into psi-filtered-vars

parents 0bfbb49c 6f114149
......@@ -43,16 +43,16 @@ code_quality:
# We need to duplicate this job's definition because it seems it's impossible to
# override an included `only.refs`.
# See https://gitlab.com/gitlab-org/gitlab/issues/31371.
# Once https://gitlab.com/gitlab-org/gitlab/merge_requests/16487 will be deployed
# to GitLab.com, we should be able to use the template and set SAST_DISABLE_DIND: "true".
sast:
.sast:
extends:
- .default-retry
- .reports:rules:sast
- .use-docker-in-docker
stage: test
allow_failure: true
# `needs: []` starts the job immediately in the pipeline
# https://docs.gitlab.com/ee/ci/yaml/README.html#needs
needs: []
allow_failure: true
artifacts:
paths:
- gl-sast-report.json # GitLab-specific
......@@ -63,22 +63,39 @@ sast:
# emptying DOCKER_HOST so it can be detected properly on kubernetes executor
# with the script below
DOCKER_HOST: ""
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_ANALYZER_IMAGE_TAG: 2
SAST_BRAKEMAN_LEVEL: 2 # GitLab-specific
SAST_EXCLUDED_PATHS: qa,spec,doc,ee/spec # GitLab-specific
script:
- export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
- |
if ! docker info &>/dev/null; then
if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- |
ENVS=`printenv | grep -vE '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '`
docker run "$ENVS" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
- /analyzer run
brakeman-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
eslint-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
kubesec-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
nodejs-scan-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
secrets-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
# We need to duplicate this job's definition because it seems it's impossible to
# override an included `only.refs`.
......
......@@ -20,12 +20,11 @@ build-qa-image:
- time docker build --cache-from "${QA_MASTER_IMAGE}" --tag ${QA_IMAGE} --file ./qa/Dockerfile ./
- time docker push ${QA_IMAGE}
review-cleanup:
.review-cleanup-base:
extends:
- .default-retry
- .review:rules:review-cleanup
stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true
environment:
name: review/auto-cleanup
......@@ -36,6 +35,18 @@ review-cleanup:
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
review-cleanup:
extends:
- .review-cleanup-base
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
review-cleanup-helm3:
extends:
- .review-cleanup-base
variables:
HELM_3: 1
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
review-gcp-cleanup:
extends:
- .review:rules:review-gcp-cleanup
......
......@@ -409,7 +409,6 @@ linters:
- 'ee/app/views/projects/push_rules/_index.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- 'ee/app/views/projects/services/prometheus/_metrics.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_additional_email_text.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
......
......@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
export default {
......@@ -73,6 +74,11 @@ export default {
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
// If no issues are opened, close all sidebars first
if (!boardsStore.detail?.issue?.id) {
sidebarEventHub.$emit('sidebar.closeAll');
}
// If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
......
......@@ -103,12 +103,14 @@ export default Vue.extend({
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
eventHub.$on('sidebar.closeAll', this.closeSidebar);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
eventHub.$off('sidebar.closeAll', this.closeSidebar);
},
mounted() {
new IssuableContext(this.currentUser);
......
......@@ -112,7 +112,6 @@ export default {
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
mergeRequestDiff: state => state.diffs.mergeRequestDiff,
commit: state => state.diffs.commit,
targetBranchName: state => state.diffs.targetBranchName,
renderOverflowWarning: state => state.diffs.renderOverflowWarning,
numTotalFiles: state => state.diffs.realSize,
numVisibleFiles: state => state.diffs.size,
......@@ -123,19 +122,9 @@ export default {
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
versionIndex: -1,
path: '',
};
},
canCurrentUserFork() {
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
},
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
renderDiffFiles() {
return (
this.diffFiles.length > 0 ||
......@@ -369,8 +358,6 @@ export default {
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch"
:is-limited-container="isLimitedContainer"
:diff-files-length="diffFilesLength"
/>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
......@@ -10,98 +8,14 @@ export default {
TimeAgo,
},
props: {
otherVersions: {
versions: {
type: Array,
required: false,
default: () => [],
},
mergeRequestVersion: {
type: Object,
required: false,
default: null,
},
startVersion: {
type: Object,
required: false,
default: null,
},
targetBranch: {
type: Object,
required: false,
default: null,
},
showCommitCount: {
type: Boolean,
required: false,
default: false,
},
baseVersionPath: {
type: String,
required: false,
default: null,
required: true,
},
},
computed: {
targetVersions() {
if (this.mergeRequestVersion) {
return this.otherVersions;
}
return [...this.otherVersions, this.targetBranch];
},
selectedVersionName() {
const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
return this.versionName(selectedVersion);
},
},
methods: {
commitsText(version) {
return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
return this.baseVersionPath;
}
if (this.showCommitCount) {
return version.version_path;
}
return version.compare_path;
},
versionName(version) {
if (this.isLatest(version)) {
return __('latest version');
}
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
return false;
}
if (this.targetBranch) {
return (
(this.isBase(version) && !this.startVersion) ||
(this.startVersion && this.startVersion.version_index === version.version_index)
);
}
return version.version_index === this.mergeRequestVersion.version_index;
},
isBase(version) {
if (!version || !this.targetBranch) {
return false;
}
return version.versionIndex === -1;
},
isHead() {
return parseBoolean(getParameterByName('diff_head'));
},
isLatest(version) {
return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
);
return this.versions.find(x => x.selected)?.versionName || '';
},
},
};
......@@ -120,13 +34,15 @@ export default {
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
<ul>
<li v-for="version in targetVersions" :key="version.id">
<a :class="{ 'is-active': isActive(version) }" :href="href(version)">
<li v-for="version in versions" :key="version.id">
<a :class="{ 'is-active': version.selected }" :href="version.href">
<div>
<strong>
{{ versionName(version) }}
<template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
<template v-else-if="isBase(version)">{{
{{ version.versionName }}
<template v-if="version.isHead">{{
s__('DiffsCompareBaseBranch|(HEAD)')
}}</template>
<template v-else-if="version.isBase">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
......@@ -136,8 +52,8 @@ export default {
</div>
<div>
<small>
<template v-if="showCommitCount">
{{ commitsText(version) }}
<template v-if="version.commitsText">
{{ version.commitsText }}
</template>
<time-ago
v-if="version.created_at"
......
......@@ -4,14 +4,14 @@ import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitl
import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
CompareVersionsDropdown,
CompareDropdownLayout,
Icon,
GlLink,
GlDeprecatedButton,
......@@ -27,16 +27,6 @@ export default {
type: Array,
required: true,
},
mergeRequestDiff: {
type: Object,
required: false,
default: () => ({}),
},
targetBranch: {
type: Object,
required: false,
default: null,
},
isLimitedContainer: {
type: Boolean,
required: false,
......@@ -48,7 +38,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['hasCollapsedFile']),
...mapGetters('diffs', [
'hasCollapsedFile',
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapState('diffs', [
'commit',
'showTreeList',
......@@ -57,18 +51,12 @@ export default {
'addedLines',
'removedLines',
]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
toggleFileBrowserTitle() {
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
},
baseVersionPath() {
return this.mergeRequestDiff.base_version_path;
},
},
created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
......@@ -113,19 +101,14 @@ export default {
:message="s__('MergeRequest|Compare %{source} and %{target}')"
>
<template #source>
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
<compare-dropdown-layout
:versions="diffCompareDropdownSourceVersions"
class="mr-version-dropdown"
/>
</template>
<template #target>
<compare-versions-dropdown
:other-versions="comparableDiffs"
:base-version-path="baseVersionPath"
:start-version="startVersion"
:target-branch="targetBranch"
<compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown"
/>
</template>
......
......@@ -58,3 +58,5 @@ export const START_RENDERING_INDEX = 200;
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
import { __, n__ } from '~/locale';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
......
import { __, n__, sprintf } from '~/locale';
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '../constants';
export const selectedTargetIndex = state =>
state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX;
export const selectedSourceIndex = state => state.mergeRequestDiff.version_index;
export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
const baseVersion = {
versionName: state.targetBranchName,
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
href: state.mergeRequestDiff.base_version_path,
isBase: true,
selected: !state.startVersion,
};
// Appended properties here are to make the compare_dropdown_layout easier to reason about
const formatVersion = v => {
return {
href: v.compare_path,
versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedTargetIndex,
...v,
};
};
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
// Appended properties here are to make the compare_dropdown_layout easier to reason about
return state.mergeRequestDiffs.map((v, i) => ({
...v,
href: v.version_path,
commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count),
versionName:
i === 0
? __('latest version')
: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedSourceIndex,
}));
};
......@@ -15,7 +15,7 @@ export default () => ({
endpoint: '',
basePath: '',
commit: null,
startVersion: null,
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diffFiles: [],
coverageFiles: {},
mergeRequestDiffs: [],
......
......@@ -326,6 +326,7 @@ export default {
},
[types.SET_SHOW_WHITESPACE](state, showWhitespace) {
state.showWhitespace = showWhitespace;
state.diffFiles = [];
},
[types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
state.fileFinderVisible = visible;
......
......@@ -5,6 +5,7 @@ import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/**
* Updates todo counter when todos are toggled.
......@@ -73,6 +74,24 @@ function initStatusTriggers() {
}
}
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-ci-minutes-link');
if (el && buyEl) {
const { trackLabel, trackProperty } = buyEl.dataset;
const trackEvent = 'show_buy_ci_minutes';
$(el).on('shown.bs.dropdown', () => {
Tracking.event(undefined, trackEvent, {
label: trackLabel,
property: trackProperty,
});
});
}
}
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers);
initNavUserDropdownTracking();
});
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* singleAttributeLabel('app', {__name__: "up", app: "prometheus"}) -> "app: prometheus"
*/
const singleAttributeLabel = (queryLabel, metricAttributes) => {
if (!queryLabel) return '';
const relevantAttribute = queryLabel.toLowerCase().replace(' ', '_');
const value = metricAttributes[relevantAttribute];
if (!value) return '';
return `${queryLabel}: ${value}`;
};
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* templatedLabel('__name__', {__name__: "up", app: "prometheus"}) -> "__name__"
*/
const templatedLabel = (queryLabel, metricAttributes) => {
if (!queryLabel) return '';
// eslint-disable-next-line array-callback-return
Object.entries(metricAttributes).map(([templateVar, label]) => {
const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
// eslint-disable-next-line no-param-reassign
queryLabel = queryLabel.replace(regex, label);
});
return queryLabel;
};
/**
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* multiMetricLabel('', {__name__: "up", app: "prometheus"}) -> "__name__: up, app: prometheus"
*/
const multiMetricLabel = metricAttributes => {
return Object.entries(metricAttributes)
.map(([templateVar, label]) => `${templateVar}: ${label}`)
.join(', ');
};
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
*/
const getSeriesLabel = (queryLabel, metricAttributes) => {
return (
singleAttributeLabel(queryLabel, metricAttributes) ||
templatedLabel(queryLabel, metricAttributes) ||
multiMetricLabel(metricAttributes) ||
queryLabel
);
};
/**
* @param {Array} queryResults - Array of Result objects
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
......@@ -12,21 +72,11 @@ export const makeDataSeries = (queryResults, defaultConfig) =>
if (!data.length) {
return null;
}
const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
const name = result.metric[relevantMetric];
const series = { data };
if (name) {
series.name = `${defaultConfig.name}: ${name}`;
} else {
series.name = defaultConfig.name;
Object.keys(result.metric).forEach(templateVar => {
const value = result.metric[templateVar];
const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
series.name = series.name.replace(regex, value);
});
}
return { ...defaultConfig, ...series };
return {
...defaultConfig,
...series,
name: getSeriesLabel(defaultConfig.name, result.metric),
};
})
.filter(series => series !== null);
......@@ -25,7 +25,7 @@ export default {
return Promise.resolve(file.content);
}
if (file.raw) {
if (file.raw || !file.rawPath) {
return Promise.resolve(file.raw);
}
......
<script>
import getJiraProjects from '../queries/getJiraProjects.query.graphql';
import JiraImportForm from './jira_import_form.vue';
import JiraImportSetup from './jira_import_setup.vue';
export default {
name: 'JiraImportApp',
components: {
JiraImportForm,
JiraImportSetup,
},
props: {
isJiraConfigured: {
type: Boolean,
required: true,
},
projectPath: {
type: String,
required: true,
},
setupIllustration: {
type: String,
required: true,
},
},
apollo: {
getJiraImports: {
......@@ -18,11 +32,17 @@ export default {
};
},
update: data => data.project.jiraImports,
skip() {
return !this.isJiraConfigured;
},
},
},
};
</script>
<template>
<div></div>
<div>
<jira-import-setup v-if="!isJiraConfigured" :illustration="setupIllustration" />
<jira-import-form v-else />
</div>
</template>
<script>
import { GlAvatar, GlNewButton, GlFormGroup, GlFormSelect, GlLabel } from '@gitlab/ui';
export default {
name: 'JiraImportForm',
components: {
GlAvatar,
GlNewButton,
GlFormGroup,
GlFormSelect,
GlLabel,
},
currentUserAvatarUrl: gon.current_user_avatar_url,
currentUsername: gon.current_username,
};
</script>
<template>
<div>
<h3 class="page-title">{{ __('New Jira import') }}</h3>
<hr />
<form>
<gl-form-group
class="row align-items-center"
:label="__('Import from')"
label-cols-sm="2"
label-for="jira-project-select"
>
<gl-form-select id="jira-project-select" class="mb-2" />
</gl-form-group>
<gl-form-group
class="row align-items-center"
:label="__('Issue label')"
label-cols-sm="2"
label-for="jira-project-label"
>
<gl-label
id="jira-project-label"
class="mb-2"
background-color="#428BCA"
title="jira-import::KEY-1"
scoped
/>
</gl-form-group>
<hr />
<p class="offset-md-1">
{{
__(
"For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:",
)
}}
</p>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Title')"
label-cols-sm="2"
label-for="jira-project-title"
>
<p id="jira-project-title" class="mb-2">{{ __('jira.issue.summary') }}</p>
</gl-form-group>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Reporter')"
label-cols-sm="2"
label-for="jira-project-reporter"
>
<gl-avatar
id="jira-project-reporter"
class="mb-2"
:src="$options.currentUserAvatarUrl"
:size="24"
:aria-label="$options.currentUsername"
/>
</gl-form-group>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Description')"
label-cols-sm="2"
label-for="jira-project-description"
>
<p id="jira-project-description" class="mb-2">{{ __('jira.issue.description.content') }}</p>
</gl-form-group>
<div class="footer-block row-content-block d-flex justify-content-between">
<gl-new-button category="primary" variant="success">{{ __('Next') }}</gl-new-button>
<gl-new-button>{{ __('Cancel') }}</gl-new-button>
</div>
</form>
</div>
</template>
<script>
export default {
name: 'JiraImportSetup',
props: {
illustration: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="empty-state">
<div class="svg-content">
<img :src="illustration" :alt="__('Set up Jira Integration illustration')" />
</div>
<div class="text-content d-flex flex-column align-items-center">
<p>{{ __('You will first need to set up Jira Integration to use this feature.') }}</p>
<a class="btn btn-success" href="../services/jira/edit">
{{ __('Set up Jira Integration') }}
</a>
</div>
</div>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/jira_import_app.vue';
Vue.use(VueApollo);
......@@ -23,7 +24,9 @@ export default function mountJiraImportApp() {
render(createComponent) {
return createComponent(App, {
props: {
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
projectPath: el.dataset.projectPath,
setupIllustration: el.dataset.setupIllustration,
},
});
},
......
......@@ -87,11 +87,17 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] }
return {
name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos],
// style options
symbol: deployment.icon,
symbolSize: symbolSizes.default,
itemStyle: {
color: deployment.color,
},
// metadata that are accessible in `formatTooltipText` method
tooltipData: {
sha: deployment.sha.substring(0, 8),
commitUrl: deployment.commitUrl,
},
};
});
......@@ -100,8 +106,12 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] }
return {
name: 'annotations',
value: [annotation.from, annotationsYAxisCoords.pos],
// style options
symbol: 'none',
description: annotation.description,
// metadata that are accessible in `formatTooltipText` method
tooltipData: {
description: annotation.description,
},
};
});
......
......@@ -58,7 +58,7 @@ export default {
},
methods: {
formatLegendLabel(query) {
return `${query.label}`;
return query.label;
},
onResize() {
if (!this.$refs.barChart) return;
......
......@@ -76,7 +76,7 @@ export default {
},
methods: {
formatLegendLabel(query) {
return `${query.label}`;
return query.label;
},
onResize() {
if (!this.$refs.columnChart) return;
......
......@@ -251,7 +251,7 @@ export default {
},
methods: {
formatLegendLabel(query) {
return `${query.label}`;
return query.label;
},
isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType;
......@@ -262,19 +262,17 @@ export default {
params.seriesData.forEach(dataPoint => {
if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value;
const [, yVal] = dataPoint.value;
this.tooltip.type = dataPoint.name;
if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === xVal,
);
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
const { data = {} } = dataPoint;
this.tooltip.sha = data?.tooltipData?.sha;
this.tooltip.commitUrl = data?.tooltipData?.commitUrl;
} else if (
this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations)
) {
const { data } = dataPoint;
this.tooltip.content.push(data?.description);
this.tooltip.content.push(data?.tooltipData?.description);
} else {
const { seriesName, color, dataIndex } = dataPoint;
......
......@@ -68,12 +68,11 @@ export const parseEnvironmentsResponse = (response = [], projectPath) =>
* https://gitlab.com/gitlab-org/gitlab/issues/207198
*
* @param {Array} metrics - Array of prometheus metrics
* @param {String} defaultLabel - Default label for metrics
* @returns {Object}
*/
const mapToMetricsViewModel = (metrics, defaultLabel) =>
const mapToMetricsViewModel = metrics =>
metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({
label: label || defaultLabel,
label,
queryRange: query_range,
prometheusEndpointPath: prometheus_endpoint_path,
metricId: uniqMetricsId({ metric_id, id }),
......
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default {
components: {
timeAgoTooltip,
GitlabTeamMemberBadge,
},
props: {
author: {
......@@ -48,6 +50,9 @@ export default {
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
showGitlabTeamMemberBadge() {
return this.author?.is_gitlab_employee;
},
},
methods: {
...mapActions(['setTargetNoteHash']),
......@@ -73,19 +78,21 @@ export default {
{{ __('Toggle thread') }}
</button>
</div>
<a
v-if="hasAuthor"
v-once
:href="author.path"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
<slot name="note-header-info"></slot>
<span class="note-header-author-name bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
</a>
<template v-if="hasAuthor">
<a
v-once
:href="author.path"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
<slot name="note-header-info"></slot>
<span class="note-header-author-name bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
</a>
<gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
......
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PrometheusMetrics from '~/prometheus_metrics/custom_metrics';
import PrometheusAlerts from '~/prometheus_alerts';
import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
prometheusMetrics.init();
}
PrometheusAlerts();
......
......@@ -2,4 +2,5 @@ export default {
EMPTY: 'empty',
LOADING: 'loading',
LIST: 'list',
NO_INTEGRATION: 'no-integration',
};
import $ from 'jquery';
import { escape, sortBy } from 'lodash';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PrometheusMetrics from './prometheus_metrics';
import PANEL_STATE from './constants';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export default class EEPrometheusMetrics extends PrometheusMetrics {
export default class CustomMetrics extends PrometheusMetrics {
constructor(wrapperSelector) {
super(wrapperSelector);
this.customMetrics = [];
......@@ -55,6 +55,14 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
this.isServiceActive = this.$monitoredCustomMetricsPanel.data('service-active');
}
init() {
if (this.isServiceActive) {
this.loadActiveCustomMetrics();
} else {
this.setNoIntegrationActiveState();
}
}
// eslint-disable-next-line class-methods-use-this
setHidden(els) {
els.forEach(el => el.addClass('hidden'));
......@@ -98,7 +106,7 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
const sortedMetrics = sortBy(this.customMetrics.map(capitalizeGroup), ['group', 'title']);
sortedMetrics.forEach(metric => {
this.$monitoredCustomMetricsList.append(EEPrometheusMetrics.customMetricTemplate(metric));
this.$monitoredCustomMetricsList.append(CustomMetrics.customMetricTemplate(metric));
});
this.$monitoredCustomMetricsCount.text(this.customMetrics.length);
......
......@@ -28,6 +28,10 @@ export default class PrometheusMetrics {
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
init() {
this.loadActiveMetrics();
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
......
......@@ -53,11 +53,19 @@ export default {
/>
<ci-icon v-else :status="iconStatus" :size="24" />
</div>
<div class="report-block-list-issue-description">
<div class="report-block-list-issue-description-text">{{ summary }}</div>
<popover v-if="popoverOptions" :options="popoverOptions" />
<div class="report-block-list-issue-description-text">
{{ summary
}}<span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span>
</div>
</div>
<div
v-if="$slots.default"
class="text-right flex-fill d-flex justify-content-end flex-column flex-sm-row"
>
<slot></slot>
</div>
</div>
</template>
......@@ -12,7 +12,7 @@ export default function createRouter(base, baseRef) {
base: joinPaths(gon.relative_url_root || '', base),
routes: [
{
path: `(/-)?/tree/(${encodeURIComponent(baseRef)}|${baseRef})/:path*`,
path: `(/-)?/tree/(${encodeURIComponent(baseRef).replace(/%2F/g, '/')}|${baseRef})/:path*`,
name: 'treePath',
component: TreePage,
props: route => ({
......
<script>
import { GlNewButton, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlNewButton,
GlLoadingIcon,
},
props: {
saveable: {
type: Boolean,
required: false,
default: false,
},
savingChanges: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="d-flex bg-light border-top justify-content-between align-items-center py-3 px-4">
<gl-loading-icon :class="{ invisible: !savingChanges }" size="md" />
<gl-new-button
variant="success"
:disabled="!saveable || savingChanges"
@click="$emit('submit')"
>
{{ __('Submit Changes') }}
</gl-new-button>
</div>
</template>
......@@ -3,27 +3,29 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoader } from '@gitlab/ui';
import EditArea from './edit_area.vue';
import Toolbar from './publish_toolbar.vue';
export default {
components: {
EditArea,
GlSkeletonLoader,
Toolbar,
},
computed: {
...mapState(['content', 'isLoadingContent']),
...mapGetters(['isContentLoaded']),
...mapState(['content', 'isLoadingContent', 'isSavingChanges']),
...mapGetters(['isContentLoaded', 'contentChanged']),
},
mounted() {
this.loadContent();
},
methods: {
...mapActions(['loadContent']),
...mapActions(['loadContent', 'setContent', 'submitChanges']),
},
};
</script>
<template>
<div class="d-flex justify-content-center h-100">
<div v-if="isLoadingContent" class="w-50 h-50 mt-2">
<div class="d-flex justify-content-center h-100 pt-2">
<div v-if="isLoadingContent" class="w-50 h-50">
<gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" />
......@@ -33,6 +35,17 @@ export default {
<rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<edit-area v-if="isContentLoaded" class="w-75 h-100 shadow-none" :value="content" />
<div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
<edit-area
class="w-75 h-100 shadow-none align-self-center"
:value="content"
@input="setContent"
/>
<toolbar
:saveable="contentChanged"
:saving-changes="isSavingChanges"
@submit="submitChanges"
/>
</div>
</div>
</template>
......@@ -6,7 +6,7 @@ const initStaticSiteEditor = el => {
const { projectId, path: sourcePath } = el.dataset;
const store = createStore({
initialState: { projectId, sourcePath },
initialState: { projectId, sourcePath, username: window.gon.current_username },
});
return new Vue({
......
// TODO implement
const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000));
export default submitContentChanges;
......@@ -3,6 +3,7 @@ import { __ } from '~/locale';
import * as mutationTypes from './mutation_types';
import loadSourceContent from '~/static_site_editor/services/load_source_content';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
export const loadContent = ({ commit, state: { sourcePath, projectId } }) => {
commit(mutationTypes.LOAD_CONTENT);
......@@ -15,4 +16,19 @@ export const loadContent = ({ commit, state: { sourcePath, projectId } }) => {
});
};
export const setContent = ({ commit }, content) => {
commit(mutationTypes.SET_CONTENT, content);
};
export const submitChanges = ({ state: { projectId, content, sourcePath, username }, commit }) => {
commit(mutationTypes.SUBMIT_CHANGES);
return submitContentChanges({ content, projectId, sourcePath, username })
.then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data))
.catch(error => {
commit(mutationTypes.SUBMIT_CHANGES_ERROR);
createFlash(error.message);
});
};
export default () => {};
// eslint-disable-next-line import/prefer-default-export
export const isContentLoaded = ({ content }) => Boolean(content);
export const isContentLoaded = ({ originalContent }) => Boolean(originalContent);
export const contentChanged = ({ originalContent, content }) => originalContent !== content;
export const LOAD_CONTENT = 'loadContent';
export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess';
export const RECEIVE_CONTENT_ERROR = 'receiveContentError';
export const SET_CONTENT = 'setContent';
export const SUBMIT_CHANGES = 'submitChanges';
export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess';
export const SUBMIT_CHANGES_ERROR = 'submitChangesError';
......@@ -8,8 +8,23 @@ export default {
state.isLoadingContent = false;
state.title = title;
state.content = content;
state.originalContent = content;
},
[types.RECEIVE_CONTENT_ERROR](state) {
state.isLoadingContent = false;
},
[types.SET_CONTENT](state, content) {
state.content = content;
},
[types.SUBMIT_CHANGES](state) {
state.isSavingChanges = true;
},
[types.SUBMIT_CHANGES_SUCCESS](state, meta) {
state.savedContentMeta = meta;
state.isSavingChanges = false;
state.originalContent = state.content;
},
[types.SUBMIT_CHANGES_ERROR](state) {
state.isSavingChanges = false;
},
};
const createState = (initialState = {}) => ({
username: null,
projectId: null,
sourcePath: null,
isLoadingContent: false,
isSavingChanges: false,
originalContent: '',
content: '',
title: '',
......
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
export default {
name: 'GitlabTeamMemberBadge',
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
};
</script>
<template>
<span
v-gl-tooltip.hover
:title="$options.gitlabTeamMemberLabel"
role="img"
:aria-label="$options.gitlabTeamMemberLabel"
class="d-inline-block align-middle"
>
<gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
</span>
</template>
......@@ -210,3 +210,15 @@
}
}
}
.health-status {
.dropdown-body {
.health-divider {
border-top-color: $gray-200;
}
.dropdown-item:not(.health-dropdown-item) {
padding: 0;
}
}
}
......@@ -63,15 +63,6 @@
list-style: none;
padding: 0 1px;
margin: 0;
.license-item {
line-height: $gl-padding-32;
.license-packages {
font-size: $label-font-size;
}
}
}
.report-block-list-icon {
......
......@@ -6,18 +6,20 @@ class GroupsController < Groups::ApplicationController
include ParamsBackwardCompatibility
include PreviewMarkdown
include RecordUserLastActivity
include SendFileUpload
extend ::Gitlab::Utils::Override
respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :ensure_export_enabled, only: [:export, :download_export]
before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
# Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer, :export, :download_export]
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
......@@ -29,6 +31,8 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, @group)
end
before_action :export_rate_limit, only: [:export, :download_export]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
......@@ -134,6 +138,25 @@ class GroupsController < Groups::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
def export
export_service = Groups::ImportExport::ExportService.new(group: @group, user: current_user)
if export_service.async_execute
redirect_to edit_group_path(@group), notice: _('Group export started.')
else
redirect_to edit_group_path(@group), alert: _('Group export could not be started.')
end
end
def download_export
if @group.export_file_exists?
send_upload(@group.export_file, attachment: @group.export_file.filename)
else
redirect_to edit_group_path(@group),
alert: _('Group export link has expired. Please generate a new export from your group settings.')
end
end
protected
def render_show_html
......@@ -234,6 +257,21 @@ class GroupsController < Groups::ApplicationController
url_for(safe_params)
end
def export_rate_limit
prefixed_action = "group_#{params[:action]}".to_sym
if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, @group])
Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to edit_group_path(@group)
end
end
def ensure_export_enabled
render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
end
private
def groups
......
......@@ -7,9 +7,10 @@ module Projects
before_action :jira_integration_configured?
def show
@is_jira_configured = @project.jira_service.present?
return if Feature.enabled?(:jira_issue_import_vue, @project)
unless @project.import_state&.in_progress?
unless @project.latest_jira_import&.in_progress?
jira_client = @project.jira_service.client
jira_projects = jira_client.Project.all
......@@ -20,7 +21,7 @@ module Projects
end
end
flash[:notice] = _("Import %{status}") % { status: @project.import_state.status } if @project.import_state.present? && !@project.import_state.none?
flash[:notice] = _("Import %{status}") % { status: @project.jira_import_status } unless @project.latest_jira_import&.initial?
end
def import
......@@ -39,12 +40,13 @@ module Projects
private
def jira_import_enabled?
return if Feature.enabled?(:jira_issue_import, @project)
return if @project.jira_issues_import_feature_flag_enabled?
redirect_to project_issues_path(@project)
end
def jira_integration_configured?
return if Feature.enabled?(:jira_issue_import_vue, @project)
return if @project.jira_service
flash[:notice] = _("Configure the Jira integration first on your project's %{strong_start} Settings > Integrations > Jira%{strong_end} page." %
......
......@@ -180,7 +180,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: TestReportSerializer
.new(current_user: @current_user)
.represent(test_reports)
.represent(test_reports, project: project)
end
end
end
......
......@@ -13,8 +13,6 @@
class LicenseTemplateFinder
include Gitlab::Utils::StrongMemoize
prepend_if_ee('::EE::LicenseTemplateFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
attr_reader :project, :params
def initialize(project, params = {})
......@@ -52,3 +50,5 @@ class LicenseTemplateFinder
params.fetch(:popular, nil)
end
end
LicenseTemplateFinder.prepend_if_ee('::EE::LicenseTemplateFinder')
......@@ -27,8 +27,6 @@
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
prepend_if_ee('::EE::ProjectsFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
attr_accessor :params
attr_reader :current_user, :project_ids_relation
......@@ -225,3 +223,5 @@ class ProjectsFinder < UnionFinder
{ min_access_level: params[:min_access_level] }
end
end
ProjectsFinder.prepend_if_ee('::EE::ProjectsFinder')
......@@ -3,8 +3,6 @@
class TemplateFinder
include Gitlab::Utils::StrongMemoize
prepend_if_ee('::EE::TemplateFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
VENDORED_TEMPLATES = HashWithIndifferentAccess.new(
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
......@@ -42,3 +40,5 @@ class TemplateFinder
end
end
end
TemplateFinder.prepend_if_ee('::EE::TemplateFinder')
......@@ -30,11 +30,11 @@ module Mutations
service_response = ::JiraImport::StartImportService
.new(context[:current_user], project, jira_project_key)
.execute
import_data = service_response.payload[:import_data]
jira_import = service_response.success? ? service_response.payload[:import_data] : nil
errors = service_response.error? ? [service_response.message] : []
{
jira_import: import_data.errors.blank? ? import_data.projects.last : nil,
errors: errors_on_object(import_data)
jira_import: jira_import,
errors: errors
}
end
......
# frozen_string_literal: true
module Resolvers
module Metrics
class DashboardResolver < Resolvers::BaseResolver
argument :path, GraphQL::STRING_TYPE,
required: true,
description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'"
type Types::Metrics::DashboardType, null: true
alias_method :environment, :object
def resolve(**args)
return unless environment
::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end
end
end
end
......@@ -8,15 +8,13 @@ module Resolvers
alias_method :project, :object
def resolve(**args)
return JiraImportData.none unless project&.import_data.present?
authorize!(project)
project.import_data.becomes(JiraImportData).projects
project.jira_imports
end
def authorized_resource?(project)
return false unless Feature.enabled?(:jira_issue_import, project)
return false unless project.jira_issues_import_feature_flag_enabled?
Ability.allowed?(context[:current_user], :admin_project, project)
end
......
......@@ -15,5 +15,9 @@ module Types
field :state, GraphQL::STRING_TYPE, null: false,
description: 'State of the environment, for example: available/stopped'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
end
end
......@@ -8,20 +8,12 @@ module Types
graphql_name 'JiraImport'
field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created/started'
method: :created_at,
description: 'Timestamp of when the Jira import was created'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
description: 'Project key for the imported Jira project',
method: :key
def scheduled_at
DateTime.parse(object.scheduled_at)
end
def scheduled_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.scheduled_by['user_id']).find
end
description: 'Project key for the imported Jira project'
end
# rubocop: enable Graphql/AuthorizeTypes
end
# frozen_string_literal: true
module Types
module Metrics
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is performed at environment level
class DashboardType < ::Types::BaseObject
graphql_name 'MetricsDashboard'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'Path to a file with the dashboard definition'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
......@@ -41,6 +41,23 @@ module ClustersHelper
end
end
def cluster_type_label(cluster_type)
case cluster_type
when 'project_type'
s_('ClusterIntegration|Project cluster')
when 'group_type'
s_('ClusterIntegration|Group cluster')
when 'instance_type'
s_('ClusterIntegration|Instance cluster')
else
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
ArgumentError.new('Cluster Type Missing'),
cluster_error: { error: 'Cluster Type Missing', cluster_type: cluster_type }
)
_('Cluster')
end
end
def has_rbac_enabled?(cluster)
return cluster.platform_kubernetes_rbac? if cluster.platform_kubernetes
......
......@@ -254,5 +254,3 @@ class Blob < SimpleDelegator
classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end
end
Blob.prepend_if_ee('EE::Blob')
......@@ -67,7 +67,6 @@ module Ci
end
def from_needs(scope)
return scope unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
return scope unless processable.scheduling_type_dag?
needs_names = processable.needs.artifacts.select(:name)
......
......@@ -25,8 +25,6 @@ module Ci
end
def self.select_with_aggregated_needs(project)
return all unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
aggregated_needs_names = Ci::BuildNeed
.scoped_build
.select("ARRAY_AGG(name)")
......
......@@ -123,6 +123,7 @@ module Clusters
scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
scope :with_management_project, -> { where.not(management_project: nil) }
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
......
......@@ -16,7 +16,7 @@ module ImportState
end
def self.jid_by(project_id:, status:)
select(:jid).with_status(status).find_by(project_id: project_id)
select(:jid).where(status: status).find_by(project_id: project_id)
end
end
end
......
# frozen_string_literal: true
module WhereComposite
extend ActiveSupport::Concern
class TooManyIds < ArgumentError
LIMIT = 100
def initialize(no_of_ids)
super(<<~MSG)
At most #{LIMIT} identifier sets at a time please! Got #{no_of_ids}.
Have you considered splitting your request into batches?
MSG
end
def self.guard(collection)
n = collection.size
return collection if n <= LIMIT
raise self, n
end
end
class_methods do
# Apply a set of constraints that function as composite IDs.
#
# This is the plural form of the standard ActiveRecord idiom:
# `where(foo: x, bar: y)`, except it allows multiple pairs of `x` and
# `y` to be specified, with the semantics that translate to:
#
# ```sql
# WHERE
# (foo = x_0 AND bar = y_0)
# OR (foo = x_1 AND bar = y_1)
# OR ...
# ```
#
# or the equivalent:
#
# ```sql
# WHERE
# (foo, bar) IN ((x_0, y_0), (x_1, y_1), ...)
# ```
#
# @param permitted_keys [Array<Symbol>] The keys each hash must have. There
# must be at least one key (but really,
# it ought to be at least two)
# @param hashes [Array<#to_h>|#to_h] The constraints. Each parameter must have a
# value for the keys named in `permitted_keys`
#
# e.g.:
# ```
# where_composite(%i[foo bar], [{foo: 1, bar: 2}, {foo: 1, bar: 3}])
# ```
#
def where_composite(permitted_keys, hashes)
raise ArgumentError, 'no permitted_keys' unless permitted_keys.present?
# accept any hash-like thing, such as Structs
hashes = TooManyIds.guard(Array.wrap(hashes)).map(&:to_h)
return none if hashes.empty?
case permitted_keys.size
when 1
key = permitted_keys.first
where(key => hashes.map { |hash| hash.fetch(key) })
else
clauses = hashes.map do |hash|
permitted_keys.map do |key|
arel_table[key].eq(hash.fetch(key))
end.reduce(:and)
end
where(clauses.reduce(:or))
end
rescue KeyError
raise ArgumentError, "all arguments must contain #{permitted_keys}"
end
end
end
......@@ -2,6 +2,7 @@
class ContainerExpirationPolicy < ApplicationRecord
include Schedulable
include UsageStatistics
belongs_to :project, inverse_of: :container_expiration_policy
......
......@@ -13,6 +13,8 @@ class GroupGroupLink < ApplicationRecord
validates :group_access, inclusion: { in: Gitlab::Access.all_values },
presence: true
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
def self.access_options
Gitlab::Access.options_with_owner
end
......
......@@ -16,6 +16,7 @@ class Issue < ApplicationRecord
include LabelEventable
include IgnorableColumns
include MilestoneEventable
include WhereComposite
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......@@ -78,6 +79,26 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
# `{project_id: x, iid: y}`.
#
# @see WhereComposite::where_composite
#
# e.g:
#
# .by_project_id_and_iid({project_id: 1, iid: 2})
# .by_project_id_and_iid([]) # returns ActiveRecord::NullRelation
# .by_project_id_and_iid([
# {project_id: 1, iid: 1},
# {project_id: 2, iid: 1},
# {project_id: 1, iid: 2}
# ])
#
scope :by_project_id_and_iid, ->(composites) do
where_composite(%i[project_id iid], composites)
end
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
......
# frozen_string_literal: true
class JiraImportData < ProjectImportData
JiraProjectDetails = Struct.new(:key, :scheduled_at, :scheduled_by)
FORCE_IMPORT_KEY = 'force-import'
def projects
return [] unless data
projects = data.dig('jira', 'projects')&.map do |p|
JiraProjectDetails.new(p['key'], p['scheduled_at'], p['scheduled_by'])
end
projects&.sort_by { |jp| jp.scheduled_at } || []
end
def <<(project)
self.data ||= { 'jira' => { 'projects' => [] } }
self.data['jira'] ||= { 'projects' => [] }
self.data['jira']['projects'] = [] if data['jira']['projects'].blank? || !data['jira']['projects'].is_a?(Array)
self.data['jira']['projects'] << project.to_h
self.data.deep_stringify_keys!
end
def force_import!
self.data ||= {}
self.data.deep_merge!({ 'jira' => { FORCE_IMPORT_KEY => true } })
self.data.deep_stringify_keys!
end
def force_import?
!!data&.dig('jira', FORCE_IMPORT_KEY) && !projects.blank?
end
def finish_import!
return if data&.dig('jira', FORCE_IMPORT_KEY).nil?
data['jira'].delete(FORCE_IMPORT_KEY)
end
def current_project
projects.last
end
end
......@@ -22,6 +22,8 @@ class JiraImportState < ApplicationRecord
message: _('Cannot have multiple Jira imports running at the same time')
}
alias_method :scheduled_by, :user
state_machine :status, initial: :initial do
event :schedule do
transition initial: :scheduled
......@@ -46,6 +48,11 @@ class JiraImportState < ApplicationRecord
end
end
before_transition any => :finished do |state, _|
InternalId.flush_records!(project: state.project)
state.project.update_project_counter_caches
end
after_transition any => :finished do |state, _|
if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid)
......@@ -67,4 +74,8 @@ class JiraImportState < ApplicationRecord
def in_progress?
scheduled? || started?
end
def non_initial?
!initial?
end
end
......@@ -555,22 +555,28 @@ class MergeRequest < ApplicationRecord
end
end
def diff_stats
return unless diff_refs
strong_memoize(:diff_stats) do
project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha)
end
end
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
merge_request_diff&.real_size || diffs.real_size
merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
end
def modified_paths(past_merge_request_diff: nil)
diffs = if past_merge_request_diff
past_merge_request_diff
elsif compare
compare
else
self.merge_request_diff
end
diffs.modified_paths
if past_merge_request_diff
past_merge_request_diff.modified_paths
elsif compare
diff_stats&.paths || compare.modified_paths
else
merge_request_diff.modified_paths
end
end
def new_paths
......
......@@ -4,27 +4,41 @@ module PerformanceMonitoring
class PrometheusDashboard
include ActiveModel::Model
attr_accessor :dashboard, :panel_groups
attr_accessor :dashboard, :panel_groups, :path, :environment, :priority
validates :dashboard, presence: true
validates :panel_groups, presence: true
def self.from_json(json_content)
dashboard = new(
dashboard: json_content['dashboard'],
panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) }
)
dashboard.tap(&:validate!)
class << self
def from_json(json_content)
dashboard = new(
dashboard: json_content['dashboard'],
panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) }
)
dashboard.tap(&:validate!)
end
def find_for(project:, user:, path:, options: {})
dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
return unless dashboard_response[:status] == :success
new(
{
path: path,
environment: options[:environment]
}.merge(dashboard_response[:dashboard])
)
end
end
def to_yaml
self.as_json(only: valid_attributes).to_yaml
self.as_json(only: yaml_valid_attributes).to_yaml
end
private
def valid_attributes
def yaml_valid_attributes
%w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard)
end
end
......
......@@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusPanel
include ActiveModel::Model
attr_accessor :type, :title, :y_label, :weight, :metrics
attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis
validates :title, presence: true
validates :metrics, presence: true
......@@ -20,5 +20,9 @@ module PerformanceMonitoring
panel.tap(&:validate!)
end
def id(group_title)
Digest::SHA2.hexdigest([group_title, type, title].join)
end
end
end
......@@ -786,6 +786,10 @@ class Project < ApplicationRecord
Feature.enabled?(:context_commits, default_enabled: true)
end
def jira_issues_import_feature_flag_enabled?
Feature.enabled?(:jira_issue_import, self, default_enabled: true)
end
def team
@team ||= ProjectTeam.new(self)
end
......@@ -859,9 +863,7 @@ class Project < ApplicationRecord
end
def jira_import_status
return import_status if jira_force_import?
import_data&.becomes(JiraImportData)&.projects.blank? ? 'none' : 'finished'
latest_jira_import&.status || 'initial'
end
def human_import_status_name
......@@ -875,8 +877,6 @@ class Project < ApplicationRecord
elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id)
elsif jira_import?
Gitlab::JiraImport::Stage::StartImportWorker.perform_async(self.id)
else
RepositoryImportWorker.perform_async(self.id)
end
......@@ -909,7 +909,7 @@ class Project < ApplicationRecord
# This method is overridden in EE::Project model
def remove_import_data
import_data&.destroy unless jira_import?
import_data&.destroy
end
def ci_config_path=(value)
......@@ -972,11 +972,7 @@ class Project < ApplicationRecord
end
def jira_import?
import_type == 'jira' && Feature.enabled?(:jira_issue_import, self)
end
def jira_force_import?
jira_import? && import_data&.becomes(JiraImportData)&.force_import?
import_type == 'jira' && latest_jira_import.present? && jira_issues_import_feature_flag_enabled?
end
def gitlab_project_import?
......
......@@ -3,11 +3,6 @@
class ProjectGroupLink < ApplicationRecord
include Expirable
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
belongs_to :project
belongs_to :group
......@@ -18,6 +13,8 @@ class ProjectGroupLink < ApplicationRecord
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
after_commit :refresh_group_members_authorized_projects
alias_method :shared_with_group, :group
......@@ -27,7 +24,7 @@ class ProjectGroupLink < ApplicationRecord
end
def self.default_access
DEVELOPER
Gitlab::Access::DEVELOPER
end
def self.search(query)
......
......@@ -56,7 +56,8 @@ class PrometheusAlert < ApplicationRecord
"for" => "5m",
"labels" => {
"gitlab" => "hook",
"gitlab_alert_id" => prometheus_metric_id
"gitlab_alert_id" => prometheus_metric_id,
"gitlab_prometheus_alert_id" => id
}
}
end
......
......@@ -267,7 +267,7 @@ class Snippet < ApplicationRecord
def repository_size_checker
strong_memoize(:repository_size_checker) do
::Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { repository._uncached_size.megabytes },
current_size_proc: -> { repository.size.megabytes },
limit: Gitlab::CurrentSettings.snippet_size_limit
)
end
......
......@@ -89,3 +89,5 @@ module Clusters
end
end
end
Clusters::ClusterPresenter.prepend_if_ee('EE::Clusters::ClusterPresenter')
......@@ -26,8 +26,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
presents :build
prepend_if_ee('::EE::CommitStatusPresenter') # rubocop: disable Cop/InjectEnterpriseEditionModule
def self.callout_failure_messages
CALLOUT_FAILURE_MESSAGES
end
......@@ -44,3 +42,5 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
end
CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter')
......@@ -3,7 +3,7 @@
module Projects
module Prometheus
class AlertPresenter < Gitlab::View::Presenter::Delegated
RESERVED_ANNOTATIONS = %w(gitlab_incident_markdown title).freeze
RESERVED_ANNOTATIONS = %w(gitlab_incident_markdown gitlab_y_label title).freeze
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = IncidentManagement::CreateIssueService::INCIDENT_LABEL[:title].freeze
......
# frozen_string_literal: true
class BuildDetailsEntity < JobEntity
prepend_if_ee('::EE::BuildDetailEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags
expose :has_trace?, as: :has_trace
......@@ -152,3 +150,5 @@ class BuildDetailsEntity < JobEntity
_("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
end
end
BuildDetailsEntity.prepend_if_ee('::EE::BuildDetailEntity')
......@@ -2,7 +2,6 @@
class IssueEntity < IssuableEntity
include TimeTrackableEntity
prepend_if_ee('::EE::IssueEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :state
expose :milestone_id
......@@ -73,3 +72,5 @@ class IssueEntity < IssuableEntity
help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project')
end
end
IssueEntity.prepend_if_ee('::EE::IssueEntity')
# frozen_string_literal: true
class ProjectMirrorEntity < Grape::Entity
prepend_if_ee('::EE::ProjectMirrorEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :id
expose :remote_mirrors_attributes, using: RemoteMirrorEntity do |project|
project.remote_mirrors
end
end
ProjectMirrorEntity.prepend_if_ee('::EE::ProjectMirrorEntity')
......@@ -8,7 +8,8 @@ module Ci
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
class CompareReportsBaseService < ::BaseService
def execute(base_pipeline, head_pipeline)
comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
comparer = build_comparer(base_pipeline, head_pipeline)
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
......@@ -28,6 +29,12 @@ module Ci
data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
end
protected
def build_comparer(base_pipeline, head_pipeline)
comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
end
private
def key(base_pipeline, head_pipeline)
......
......@@ -93,7 +93,7 @@ module Ci
end
def processable_status(processable)
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && processable.scheduling_type_dag?
if processable.scheduling_type_dag?
# Processable uses DAG, get status of all dependent needs
@collection.status_for_names(processable.aggregated_needs_names.to_a)
else
......
......@@ -43,8 +43,6 @@ module Ci
end
def process_dag_builds_without_needs
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
created_processables.scheduling_type_dag.without_needs.each do |build|
process_build(build, 'success')
end
......@@ -52,7 +50,6 @@ module Ci
def process_dag_builds_with_needs(trigger_build_ids)
return false unless trigger_build_ids.present?
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
# we find processables that are dependent:
# 1. because of current dependency,
......@@ -110,11 +107,7 @@ module Ci
end
def created_stage_scheduled_processables
if Feature.enabled?(:ci_dag_support, project, default_enabled: true)
created_processables.scheduling_type_stage
else
created_processables
end
created_processables.scheduling_type_stage
end
def created_processables
......
......@@ -10,9 +10,15 @@ module Groups
@shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
end
def async_execute
GroupExportWorker.perform_async(@current_user.id, @group.id, @params)
end
def execute
validate_user_permissions
remove_existing_export! if @group.export_file_exists?
save!
ensure
cleanup
......@@ -30,6 +36,13 @@ module Groups
end
end
def remove_existing_export!
import_export_upload = @group.import_export_upload
import_export_upload.remove_export_file!
import_export_upload.save
end
def save!
if savers.all?(&:save)
notify_success
......
......@@ -20,27 +20,31 @@ module JiraImport
private
def create_and_schedule_import
import_data = project.create_or_update_import_data(data: {}).becomes(JiraImportData)
jira_project_details = JiraImportData::JiraProjectDetails.new(
jira_project_key,
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: user.id, name: user.name }
)
import_data << jira_project_details
import_data.force_import!
jira_import = build_jira_import
project.import_type = 'jira'
project.import_state.schedule if project.save!
project.save! && jira_import.schedule!
ServiceResponse.success(payload: { import_data: import_data } )
ServiceResponse.success(payload: { import_data: jira_import } )
rescue => ex
# in case project.save! raises an erorr
Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
build_error_response(ex.message)
jira_import.do_fail!
end
def build_jira_import
project.jira_imports.build(
user: user,
jira_project_key: jira_project_key,
# we do not have the jira_project_name or jira_project_xid yet so just set a mock value,
# we will once https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28190
jira_project_name: jira_project_key,
jira_project_xid: 0
)
end
def validate
return build_error_response(_('Jira import feature is disabled.')) unless Feature.enabled?(:jira_issue_import, project)
return build_error_response(_('Jira import feature is disabled.')) unless project.jira_issues_import_feature_flag_enabled?
return build_error_response(_('You do not have permissions to run the import.')) unless user.can?(:admin_project, project)
return build_error_response(_('Jira integration not configured.')) unless project.jira_service&.active?
return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank?
......@@ -48,18 +52,11 @@ module JiraImport
end
def build_error_response(message)
import_data = JiraImportData.new(project: project)
import_data.errors.add(:base, message)
ServiceResponse.error(
message: import_data.errors.full_messages.to_sentence,
http_status: 400,
payload: { import_data: import_data }
)
ServiceResponse.error(message: message, http_status: 400)
end
def import_in_progress?
import_state = project.import_state || project.create_import_state
import_state.in_progress?
project.latest_jira_import&.in_progress?
end
end
end
......@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [
STAGES::CommonMetricsInserter,
STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter
].freeze
......
......@@ -57,7 +57,7 @@ module Metrics
# @return [Hash]
override :raw_dashboard
def raw_dashboard
panels_not_found!(identifiers) if panels.empty?
panels_not_found!(identifiers) if metrics.empty?
{ 'panel_groups' => [{ 'panels' => panels }] }
end
......@@ -66,11 +66,20 @@ module Metrics
# Generated dashboard panels for each metric which
# matches the provided input.
#
# As the panel is generated
# on the fly, we're using default values for info
# not represented in the DB.
#
# @return [Array<Hash>]
def panels
strong_memoize(:panels) do
metrics.map { |metric| panel_for_metric(metric) }
end
[{
type: DEFAULT_PANEL_TYPE,
weight: DEFAULT_PANEL_WEIGHT,
title: title,
y_label: y_label,
metrics: metrics.map(&:to_metric_hash)
}]
end
# Metrics which match the provided inputs.
......@@ -78,12 +87,14 @@ module Metrics
# displayed in a single panel/chart.
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
def metrics
PrometheusMetricsFinder.new(
project: project,
group: group_key,
title: title,
y_label: y_label
).execute
strong_memoize(:metrics) do
PrometheusMetricsFinder.new(
project: project,
group: group_key,
title: title,
y_label: y_label
).execute
end
end
# Returns a symbol representing the group that
......@@ -101,22 +112,6 @@ module Metrics
.to_s
end
end
# Returns a representation of a PromtheusMetric
# as a dashboard panel. As the panel is generated
# on the fly, we're using default values for info
# not represented in the DB.
#
# @return [Hash]
def panel_for_metric(metric)
{
type: DEFAULT_PANEL_TYPE,
weight: DEFAULT_PANEL_WEIGHT,
title: metric.title,
y_label: metric.y_label,
metrics: [metric.to_metric_hash]
}
end
end
end
end
......@@ -10,7 +10,10 @@ module Metrics
class GitlabAlertEmbedService < ::Metrics::Dashboard::BaseEmbedService
include Gitlab::Utils::StrongMemoize
SEQUENCE = [STAGES::EndpointInserter].freeze
SEQUENCE = [
STAGES::EndpointInserter,
STAGES::PanelIdsInserter
].freeze
class << self
# Determines whether the provided params are sufficient
......
......@@ -10,7 +10,8 @@ module Metrics
include ReactiveCaching
SEQUENCE = [
::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter
::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter,
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter
].freeze
self.reactive_cache_key = ->(service) { service.cache_key }
......
......@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [
STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter
].freeze
......
......@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [
STAGES::CustomMetricsInserter,
STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter
].freeze
......
......@@ -13,6 +13,7 @@ module Metrics
STAGES::CustomMetricsInserter,
STAGES::CustomMetricsDetailsInserter,
STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter
].freeze
......
......@@ -3,11 +3,6 @@
class SearchService
include Gitlab::Allowable
REDACTABLE_RESULTS = [
ActiveRecord::Relation,
Gitlab::Search::FoundBlob
].freeze
SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096
......@@ -68,10 +63,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
end
def redactable_results
REDACTABLE_RESULTS
end
private
def visible_result?(object)
......@@ -80,12 +71,9 @@ class SearchService
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
end
def redact_unauthorized_results(results)
return results unless redactable_results.any? { |redactable| results.is_a?(redactable) }
permitted_results = results.select do |object|
visible_result?(object)
end
def redact_unauthorized_results(results_collection)
results = results_collection.to_a
permitted_results = results.select { |object| visible_result?(object) }
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
......@@ -93,13 +81,13 @@ class SearchService
log_redacted_search_results(filtered_results.values) if filtered_results.any?
return results.id_not_in(filtered_results.keys) if results.is_a?(ActiveRecord::Relation)
return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
Kaminari.paginate_array(
permitted_results,
total_count: results.total_count,
limit: results.limit_value,
offset: results.offset_value
total_count: results_collection.total_count,
limit: results_collection.limit_value,
offset: results_collection.offset_value
)
end
......
......@@ -4,7 +4,7 @@ module Tags
class CreateService < BaseService
def execute(tag_name, target, message)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag
return error('Tag name invalid', 400) unless valid_tag
repository = project.repository
message = message&.strip
......@@ -14,7 +14,7 @@ module Tags
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists")
return error("Tag #{tag_name} already exists", 409)
rescue Gitlab::Git::PreReceiveError => ex
return error(ex.message)
end
......@@ -24,7 +24,7 @@ module Tags
success.merge(tag: new_tag)
else
error("Target #{target} is invalid")
error("Target #{target} is invalid", 400)
end
end
end
......
......@@ -137,8 +137,6 @@ module ObjectStorage
included do |base|
base.include(ObjectStorage)
include_if_ee('::EE::ObjectStorage::Concern') # rubocop: disable Cop/InjectEnterpriseEditionModule
after :migrate, :delete_migrated_file
end
......@@ -463,3 +461,5 @@ module ObjectStorage
end
end
end
ObjectStorage::Concern.include_if_ee('::EE::ObjectStorage::Concern')
......@@ -41,7 +41,12 @@
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
%h4= @cluster.name
.d-flex.my-3
%p.badge.badge-light.p-2.mr-2
= cluster_type_label(@cluster.cluster_type)
%h4.m-0
= @cluster.name
= render 'banner'
- if cluster_created?(@cluster)
......@@ -56,7 +61,3 @@
.tab-content.py-3
.tab-pane.active{ role: 'tabpanel' }
= render_cluster_info_tab_content(params[:tab], expanded_by_default?)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment