Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4f5c8572e9
commit
a2dbe2a8d8
|
|
@ -215,10 +215,10 @@ export default {
|
|||
</div>
|
||||
<div class="form-group flex flex-wrap">
|
||||
<gl-form-checkbox :checked="wafLogEnabled" @input="wafLogChanged">
|
||||
<strong>{{ s__('ClusterIntegration|Send ModSecurity Logs') }}</strong>
|
||||
<strong>{{ s__('ClusterIntegration|Send Web Application Firewall Logs') }}</strong>
|
||||
</gl-form-checkbox>
|
||||
<gl-form-checkbox :checked="ciliumLogEnabled" @input="ciliumLogChanged">
|
||||
<strong>{{ s__('ClusterIntegration|Send Cilium Logs') }}</strong>
|
||||
<strong>{{ s__('ClusterIntegration|Send Container Network Policies Logs') }}</strong>
|
||||
</gl-form-checkbox>
|
||||
</div>
|
||||
<div v-if="showButtons" class="mt-3">
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import {
|
|||
GlIcon,
|
||||
} from '@gitlab/ui';
|
||||
import eventHub from '~/clusters/event_hub';
|
||||
import modSecurityLogo from 'images/cluster_app_logos/modsecurity.png';
|
||||
import modSecurityLogo from 'images/cluster_app_logos/gitlab.png';
|
||||
|
||||
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
|
||||
|
||||
export default {
|
||||
title: 'ModSecurity Web Application Firewall',
|
||||
title: __('Web Application Firewall'),
|
||||
modsecurityUrl: 'https://modsecurity.org/about.html',
|
||||
components: {
|
||||
GlAlert,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import ReplyButton from './note_actions/reply_button.vue';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import Api from '~/api';
|
||||
import flash from '~/flash';
|
||||
|
||||
export default {
|
||||
name: 'NoteActions',
|
||||
|
|
@ -17,6 +21,10 @@ export default {
|
|||
},
|
||||
mixins: [resolvedStatusMixin],
|
||||
props: {
|
||||
author: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
authorId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
|
@ -87,7 +95,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getUserDataByProp']),
|
||||
...mapGetters(['getUserDataByProp', 'getNoteableData']),
|
||||
shouldShowActionsDropdown() {
|
||||
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
|
||||
},
|
||||
|
|
@ -100,6 +108,26 @@ export default {
|
|||
currentUserId() {
|
||||
return this.getUserDataByProp('id');
|
||||
},
|
||||
isUserAssigned() {
|
||||
return this.assignees && this.assignees.some(({ id }) => id === this.author.id);
|
||||
},
|
||||
displayAssignUserText() {
|
||||
return this.isUserAssigned
|
||||
? __('Unassign from commenting user')
|
||||
: __('Assign to commenting user');
|
||||
},
|
||||
sidebarAction() {
|
||||
return this.isUserAssigned ? 'sidebar.addAssignee' : 'sidebar.removeAssignee';
|
||||
},
|
||||
targetType() {
|
||||
return this.getNoteableData.targetType;
|
||||
},
|
||||
assignees() {
|
||||
return this.getNoteableData.assignees || [];
|
||||
},
|
||||
isIssue() {
|
||||
return this.targetType === 'issue';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEdit() {
|
||||
|
|
@ -116,6 +144,29 @@ export default {
|
|||
this.$root.$emit('bv::hide::tooltip');
|
||||
});
|
||||
},
|
||||
handleAssigneeUpdate(assignees) {
|
||||
this.$emit('updateAssignees', assignees);
|
||||
eventHub.$emit(this.sidebarAction, this.author);
|
||||
eventHub.$emit('sidebar.saveAssignees');
|
||||
},
|
||||
assignUser() {
|
||||
let { assignees } = this;
|
||||
const { project_id, iid } = this.getNoteableData;
|
||||
|
||||
if (this.isUserAssigned) {
|
||||
assignees = assignees.filter(assignee => assignee.id !== this.author.id);
|
||||
} else {
|
||||
assignees.push({ id: this.author.id });
|
||||
}
|
||||
|
||||
if (this.targetType === 'issue') {
|
||||
Api.updateIssue(project_id, iid, {
|
||||
assignee_ids: assignees.map(assignee => assignee.id),
|
||||
})
|
||||
.then(() => this.handleAssigneeUpdate(assignees))
|
||||
.catch(() => flash(__('Something went wrong while updating assignees')));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -215,6 +266,16 @@ export default {
|
|||
<span class="text-danger">{{ __('Delete comment') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="isIssue">
|
||||
<button
|
||||
class="btn-default btn-transparent"
|
||||
data-testid="assign-user"
|
||||
type="button"
|
||||
@click="assignUser"
|
||||
>
|
||||
{{ displayAssignUserText }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ export default {
|
|||
'updateNote',
|
||||
'toggleResolveNote',
|
||||
'scrollToNoteIfNeeded',
|
||||
'updateAssignees',
|
||||
]),
|
||||
editHandler() {
|
||||
this.isEditing = true;
|
||||
|
|
@ -299,6 +300,9 @@ export default {
|
|||
getLineClasses(lineNumber) {
|
||||
return getLineClasses(lineNumber);
|
||||
},
|
||||
assigneesUpdate(assignees) {
|
||||
this.updateAssignees(assignees);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -355,6 +359,7 @@ export default {
|
|||
<span v-else-if="note.created_at" class="d-none d-sm-inline">·</span>
|
||||
</note-header>
|
||||
<note-actions
|
||||
:author="author"
|
||||
:author-id="author.id"
|
||||
:note-id="note.id"
|
||||
:note-url="note.noteable_note_url"
|
||||
|
|
@ -377,6 +382,7 @@ export default {
|
|||
@handleDelete="deleteHandler"
|
||||
@handleResolve="resolveHandler"
|
||||
@startReplying="$emit('startReplying')"
|
||||
@updateAssignees="assigneesUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-discussion-body">
|
||||
|
|
|
|||
|
|
@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => {
|
|||
commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error);
|
||||
};
|
||||
|
||||
export const updateAssignees = ({ commit }, assignees) => {
|
||||
commit(types.UPDATE_ASSIGNEES, assignees);
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH';
|
|||
export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
|
||||
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
|
||||
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
|
||||
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
|
||||
|
||||
// DISCUSSION
|
||||
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
|
||||
|
|
|
|||
|
|
@ -355,4 +355,7 @@ export default {
|
|||
[types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) {
|
||||
state.isLoadingDescriptionVersion = false;
|
||||
},
|
||||
[types.UPDATE_ASSIGNEES](state, assignees) {
|
||||
state.noteableData.assignees = assignees;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import { __ } from '~/locale';
|
||||
import { GlColumnChart } from '@gitlab/ui/dist/charts';
|
||||
import CodeCoverage from '../components/code_coverage.vue';
|
||||
import SeriesDataMixin from './series_data_mixin';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const languagesContainer = document.getElementById('js-languages-chart');
|
||||
const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
|
||||
const monthContainer = document.getElementById('js-month-chart');
|
||||
const weekdayContainer = document.getElementById('js-weekday-chart');
|
||||
const hourContainer = document.getElementById('js-hour-chart');
|
||||
|
|
@ -57,6 +59,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: codeCoverageContainer,
|
||||
render(h) {
|
||||
return h(CodeCoverage, {
|
||||
props: {
|
||||
graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: monthContainer,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<script>
|
||||
import { GlAlert, GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
|
||||
import { GlAreaChart } from '@gitlab/ui/dist/charts';
|
||||
import dateFormat from 'dateformat';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlAreaChart,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
graphEndpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dailyCoverageData: [],
|
||||
hasFetchError: false,
|
||||
isLoading: true,
|
||||
selectedCoverageIndex: 0,
|
||||
tooltipTitle: '',
|
||||
coveragePercentage: '',
|
||||
chartOptions: {
|
||||
yAxis: {
|
||||
name: __('Bi-weekly code coverage'),
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
xAxis: {
|
||||
name: '',
|
||||
type: 'category',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasData() {
|
||||
return this.dailyCoverageData.length > 0;
|
||||
},
|
||||
isReady() {
|
||||
return !this.isLoading && !this.hasFetchError;
|
||||
},
|
||||
canShowData() {
|
||||
return this.isReady && this.hasData;
|
||||
},
|
||||
noDataAvailable() {
|
||||
return this.isReady && !this.hasData;
|
||||
},
|
||||
selectedDailyCoverage() {
|
||||
return this.hasData && this.dailyCoverageData[this.selectedCoverageIndex];
|
||||
},
|
||||
selectedDailyCoverageName() {
|
||||
return this.selectedDailyCoverage?.group_name;
|
||||
},
|
||||
formattedData() {
|
||||
if (this.selectedDailyCoverage?.data) {
|
||||
return this.selectedDailyCoverage.data.map(value => [
|
||||
dateFormat(value.date, 'mmm dd'),
|
||||
value.coverage,
|
||||
]);
|
||||
}
|
||||
|
||||
// If the fetching failed, we return an empty array which
|
||||
// allow the graph to render while empty
|
||||
return [];
|
||||
},
|
||||
chartData() {
|
||||
return [
|
||||
{
|
||||
// The default string 'data' will get shown in the legend if we fail to fetch the data
|
||||
name: this.canShowData ? this.selectedDailyCoverageName : __('data'),
|
||||
data: this.formattedData,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
axios
|
||||
.get(this.graphEndpoint)
|
||||
.then(({ data }) => {
|
||||
this.dailyCoverageData = data;
|
||||
})
|
||||
.catch(() => {
|
||||
this.hasFetchError = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
setSelectedCoverage(index) {
|
||||
this.selectedCoverageIndex = index;
|
||||
},
|
||||
formatTooltipText(params) {
|
||||
this.tooltipTitle = params.value;
|
||||
this.coveragePercentage = get(params, 'seriesData[0].data[1]', '');
|
||||
},
|
||||
},
|
||||
height: 200,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-mt-3 gl-mb-3">
|
||||
<gl-alert
|
||||
v-if="hasFetchError"
|
||||
variant="danger"
|
||||
:title="s__('Code Coverage|Couldn\'t fetch the code coverage data')"
|
||||
:dismissible="false"
|
||||
/>
|
||||
<gl-alert
|
||||
v-if="noDataAvailable"
|
||||
variant="info"
|
||||
:title="s__('Code Coverage| Empty code coverage data')"
|
||||
:dismissible="false"
|
||||
>
|
||||
<span>
|
||||
{{ __('It seems that there is currently no available data for code coverage') }}
|
||||
</span>
|
||||
</gl-alert>
|
||||
<gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
|
||||
<gl-dropdown-item
|
||||
v-for="({ group_name }, index) in dailyCoverageData"
|
||||
:key="index"
|
||||
:value="group_name"
|
||||
@click="setSelectedCoverage(index)"
|
||||
>
|
||||
<div class="gl-display-flex">
|
||||
<gl-icon
|
||||
v-if="index === selectedCoverageIndex"
|
||||
name="mobile-issue-close"
|
||||
class="gl-absolute"
|
||||
/>
|
||||
<span class="gl-display-flex align-items-center ml-4">
|
||||
{{ group_name }}
|
||||
</span>
|
||||
</div>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
<gl-area-chart
|
||||
v-if="!isLoading"
|
||||
:height="$options.height"
|
||||
:data="chartData"
|
||||
:option="chartOptions"
|
||||
:format-tooltip-text="formatTooltipText"
|
||||
>
|
||||
<template v-if="canShowData" #tooltipTitle>
|
||||
{{ tooltipTitle }}
|
||||
</template>
|
||||
<template v-if="canShowData" #tooltipContent>
|
||||
<gl-sprintf :message="__('Code Coverage: %{coveragePercentage}%{percentSymbol}')">
|
||||
<template #coveragePercentage>
|
||||
{{ coveragePercentage }}
|
||||
</template>
|
||||
<template #percentSymbol>
|
||||
%
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-area-chart>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -402,7 +402,6 @@ img.emoji {
|
|||
.prepend-top-default { margin-top: $gl-padding !important; }
|
||||
.prepend-top-16 { margin-top: 16px; }
|
||||
.prepend-top-20 { margin-top: 20px; }
|
||||
.prepend-top-32 { margin-top: 32px; }
|
||||
.prepend-left-5 { margin-left: 5px; }
|
||||
.prepend-left-10 { margin-left: 10px; }
|
||||
.prepend-left-15 { margin-left: 15px; }
|
||||
|
|
|
|||
|
|
@ -149,17 +149,17 @@ $orange-800: #a35200 !default;
|
|||
$orange-900: #853c00 !default;
|
||||
$orange-950: #592800 !default;
|
||||
|
||||
$red-50: #fef6f5 !default;
|
||||
$red-100: #fbe5e1 !default;
|
||||
$red-200: #f2b4a9 !default;
|
||||
$red-300: #ea8271 !default;
|
||||
$red-400: #e05842 !default;
|
||||
$red-500: #db3b21 !default;
|
||||
$red-600: #c0341d !default;
|
||||
$red-700: #a62d19 !default;
|
||||
$red-800: #8b2615 !default;
|
||||
$red-900: #711e11 !default;
|
||||
$red-950: #4b140b !default;
|
||||
$red-50: #fcf1ef !default;
|
||||
$red-100: #fdd4cd !default;
|
||||
$red-200: #fcb5aa !default;
|
||||
$red-300: #f57f6c !default;
|
||||
$red-400: #ec5941 !default;
|
||||
$red-500: #dd2b0e !default;
|
||||
$red-600: #c91c00 !default;
|
||||
$red-700: #ae1800 !default;
|
||||
$red-800: #8d1300 !default;
|
||||
$red-900: #660e00 !default;
|
||||
$red-950: #4d0a00 !default;
|
||||
|
||||
$gray-10: #fafafa !default;
|
||||
$gray-50: #f0f0f0 !default;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ module AlertManagement
|
|||
attr_reader :alert, :current_user, :params
|
||||
|
||||
def allowed?
|
||||
current_user.can?(:update_alert_management_alert, alert)
|
||||
current_user&.can?(:update_alert_management_alert, alert)
|
||||
end
|
||||
|
||||
def assignee_todo_allowed?
|
||||
assignee&.can?(:read_alert_management_alert, alert)
|
||||
end
|
||||
|
||||
def todo_service
|
||||
|
|
@ -80,9 +84,10 @@ module AlertManagement
|
|||
end
|
||||
|
||||
def assign_todo
|
||||
return unless assignee
|
||||
# Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672
|
||||
return unless assignee_todo_allowed?
|
||||
|
||||
todo_service.assign_alert(alert, assignee)
|
||||
todo_service.assign_alert(alert, current_user)
|
||||
end
|
||||
|
||||
def add_assignee_system_note(old_assignees)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
|
||||
|
||||
- if can?(current_user, :push_code, @project)
|
||||
.empty-wrapper.prepend-top-32
|
||||
.empty-wrapper.gl-mt-7
|
||||
%h3#repo-command-line-instructions.page-title-empty
|
||||
= _('Command line instructions')
|
||||
%p
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
%a.btn.btn-sm{ href: "#{download_path}?#{@daily_coverage_options[:base_params].to_query}" }
|
||||
%small
|
||||
= _("Download raw data (.csv)")
|
||||
#js-code-coverage-chart{ data: { daily_coverage_options: @daily_coverage_options.to_json.html_safe } }
|
||||
#js-code-coverage-chart{ data: { graph_endpoint: "#{@daily_coverage_options[:graph_api_path]}?#{@daily_coverage_options[:base_params].to_query}" } }
|
||||
|
||||
.repo-charts
|
||||
.sub-header-block.border-top
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.row.prepend-top-32.append-bottom-default
|
||||
.row.gl-mt-7.append-bottom-default
|
||||
.col-lg-3
|
||||
%h4.gl-mt-0
|
||||
Recent Deliveries
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Add a button to assign users who have commented on an issue
|
||||
merge_request: 23883
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update red hex values to match GitLab UI
|
||||
merge_request: 34544
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add :section to approval_merge_request_rule unique index
|
||||
merge_request: 33121
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: GraphQL - properly handle pagination of millisecond-precision timestamps
|
||||
merge_request: 34352
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Graph code coverage changes over time for a project
|
||||
merge_request: 26174
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change from vendor specific to Gitlab
|
||||
merge_request: 34576
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Updated Auto DevOps with a fix to delete PostgreSQL PVC on environment cleanup,
|
||||
a fix for multiline K8S_SECRET variables, updated Helm to 2.16.7 and glibc to 2.31
|
||||
merge_request: 34399
|
||||
author: verenion
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateIndexApprovalRuleNameForCodeOwnersRuleType < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
LEGACY_INDEX_NAME_RULE_TYPE = "index_approval_rule_name_for_code_owners_rule_type"
|
||||
LEGACY_INDEX_NAME_CODE_OWNERS = "approval_rule_name_index_for_code_owners"
|
||||
|
||||
SECTIONAL_INDEX_NAME = "index_approval_rule_name_for_sectional_code_owners_rule_type"
|
||||
|
||||
CODE_OWNER_RULE_TYPE = 2
|
||||
|
||||
def up
|
||||
unless index_exists_by_name?(:approval_merge_request_rules, SECTIONAL_INDEX_NAME)
|
||||
# Ensure only 1 code_owner rule with the same name and section per merge_request
|
||||
#
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :name, :section],
|
||||
unique: true,
|
||||
where: "rule_type = #{CODE_OWNER_RULE_TYPE}",
|
||||
name: SECTIONAL_INDEX_NAME
|
||||
)
|
||||
end
|
||||
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :name],
|
||||
unique: true,
|
||||
where: "rule_type = #{CODE_OWNER_RULE_TYPE} AND section IS NULL",
|
||||
name: LEGACY_INDEX_NAME_RULE_TYPE
|
||||
)
|
||||
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :code_owner, :name],
|
||||
unique: true,
|
||||
where: "code_owner = true AND section IS NULL",
|
||||
name: LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# In a rollback situation, we can't guarantee that there will not be
|
||||
# records that were allowed under the more specific SECTIONAL_INDEX_NAME
|
||||
# index but would cause uniqueness violations under both the
|
||||
# LEGACY_INDEX_NAME_RULE_TYPE and LEGACY_INDEX_NAME_CODE_OWNERS indices.
|
||||
# Therefore, we need to first find all the MergeRequests with
|
||||
# ApprovalMergeRequestRules that would violate these "new" indices and
|
||||
# delete those approval rules, then create the new index, then finally
|
||||
# recreate the approval rules for those merge requests.
|
||||
#
|
||||
|
||||
# First, find all MergeRequests with ApprovalMergeRequestRules that will
|
||||
# violate the new index.
|
||||
#
|
||||
merge_request_ids = ApprovalMergeRequestRule
|
||||
.select(:merge_request_id)
|
||||
.where(rule_type: CODE_OWNER_RULE_TYPE)
|
||||
.group(:merge_request_id, :rule_type, :name)
|
||||
.includes(:merge_request)
|
||||
.having("count(*) > 1")
|
||||
.collect(&:merge_request_id)
|
||||
|
||||
# Delete ALL their code_owner approval rules
|
||||
#
|
||||
merge_request_ids.each_slice(10) do |ids|
|
||||
ApprovalMergeRequestRule.where(merge_request_id: ids).code_owner.delete_all
|
||||
end
|
||||
|
||||
# Remove legacy partial indices that only apply to `section IS NULL` records
|
||||
#
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
|
||||
# Reconstruct original "legacy" indices
|
||||
#
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :name],
|
||||
unique: true,
|
||||
where: "rule_type = #{CODE_OWNER_RULE_TYPE}",
|
||||
name: LEGACY_INDEX_NAME_RULE_TYPE
|
||||
)
|
||||
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :code_owner, :name],
|
||||
unique: true,
|
||||
where: "code_owner = true",
|
||||
name: LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
)
|
||||
|
||||
# MergeRequest::SyncCodeOwnerApprovalRules recreates the code_owner rules
|
||||
# from scratch, adding them to the index. Duplicates will be rejected.
|
||||
#
|
||||
merge_request_ids.each_slice(10) do |ids|
|
||||
MergeRequest.where(id: ids).each do |merge_request|
|
||||
MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute
|
||||
end
|
||||
end
|
||||
|
||||
remove_concurrent_index_by_name :approval_merge_request_rules, SECTIONAL_INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToComposerMetadata < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha], unique: true)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha])
|
||||
end
|
||||
end
|
||||
|
|
@ -9156,7 +9156,7 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public.
|
|||
|
||||
CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3);
|
||||
|
||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE (code_owner = true);
|
||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
|
||||
|
||||
CREATE INDEX ci_builds_gitlab_monitor_metrics ON public.ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
|
||||
|
||||
|
|
@ -9334,7 +9334,9 @@ CREATE UNIQUE INDEX index_approval_project_rules_users_1 ON public.approval_proj
|
|||
|
||||
CREATE INDEX index_approval_project_rules_users_2 ON public.approval_project_rules_users USING btree (user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE (rule_type = 2);
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE ((rule_type = 2) AND (section IS NULL));
|
||||
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
|
||||
|
||||
CREATE INDEX index_approval_rules_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2);
|
||||
|
||||
|
|
@ -10430,6 +10432,8 @@ CREATE UNIQUE INDEX index_packages_build_infos_on_package_id ON public.packages_
|
|||
|
||||
CREATE INDEX index_packages_build_infos_on_pipeline_id ON public.packages_build_infos USING btree (pipeline_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_packages_composer_metadata_on_package_id_and_target_sha ON public.packages_composer_metadata USING btree (package_id, target_sha);
|
||||
|
||||
CREATE UNIQUE INDEX index_packages_conan_file_metadata_on_package_file_id ON public.packages_conan_file_metadata USING btree (package_file_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_packages_conan_metadata_on_package_id_username_channel ON public.packages_conan_metadata USING btree (package_id, package_username, package_channel);
|
||||
|
|
@ -13959,6 +13963,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200526153844
|
||||
20200526164946
|
||||
20200526164947
|
||||
20200526231421
|
||||
20200527092027
|
||||
20200527094322
|
||||
20200527095401
|
||||
|
|
@ -13995,5 +14000,6 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200615083635
|
||||
20200615121217
|
||||
20200615123055
|
||||
20200615232735
|
||||
\.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
|
|
@ -134,15 +134,18 @@ in the jobs table.
|
|||
A few examples of known coverage tools for a variety of languages can be found
|
||||
in the pipelines settings page.
|
||||
|
||||
### Download test coverage history
|
||||
### Code Coverage history
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) in GitLab 12.10.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10.
|
||||
> - [Graph introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1.
|
||||
|
||||
If you want to see the evolution of your project code coverage over time,
|
||||
you can download a CSV file with this data. From your project:
|
||||
you can view a graph or download a CSV file with this data. From your project:
|
||||
|
||||
1. Go to **{chart}** **Project Analytics > Repository**.
|
||||
1. Click **Download raw data (`.csv`)**
|
||||
1. Go to **{chart}** **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph.
|
||||
1. If you want a CSV file of that data, click **Download raw data (.csv)**
|
||||
|
||||

|
||||
|
||||
### Removing color codes
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ The data in the charts are updated soon after each commit in the default branch.
|
|||
Available charts:
|
||||
|
||||
- Programming languages used in the repository
|
||||
- Code coverage history (last 3 months) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1)
|
||||
- Commit statistics (last month)
|
||||
- Commits per day of month
|
||||
- Commits per weekday
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
|
|
@ -532,3 +532,15 @@ to the original comment, so a note about when it was last edited will appear und
|
|||
|
||||
This feature only exists for Issues, Merge requests, and Epics. Commits, Snippets and Merge request diff threads are
|
||||
not supported yet.
|
||||
|
||||
## Assign an issue to the commenting user
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1.
|
||||
|
||||
You can assign an issue to a user who made a comment.
|
||||
|
||||
In the comment, click the **More Actions** menu and click **Assign to commenting user**.
|
||||
|
||||
Click the button again to unassign the commenter.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.dast-auto-deploy:
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0"
|
||||
|
||||
dast_environment_deploy:
|
||||
extends: .dast-auto-deploy
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.auto-deploy:
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1"
|
||||
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0"
|
||||
|
||||
include:
|
||||
- template: Jobs/Deploy/ECS.gitlab-ci.yml
|
||||
|
|
|
|||
|
|
@ -194,7 +194,12 @@ module Gitlab
|
|||
|
||||
order_list.each do |field|
|
||||
field_name = field.attribute_name
|
||||
ordering[field_name] = node[field_name].to_s
|
||||
field_value = node[field_name]
|
||||
ordering[field_name] = if field_value.is_a?(Time)
|
||||
field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z')
|
||||
else
|
||||
field_value.to_s
|
||||
end
|
||||
end
|
||||
|
||||
encode(ordering.to_json)
|
||||
|
|
|
|||
|
|
@ -2970,6 +2970,9 @@ msgstr ""
|
|||
msgid "Assign to"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign to commenting user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assign yourself to these issues"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -3415,6 +3418,9 @@ msgstr ""
|
|||
msgid "Beta"
|
||||
msgstr ""
|
||||
|
||||
msgid "Bi-weekly code coverage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5362,10 +5368,10 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Select zone to choose machine type"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Send Cilium Logs"
|
||||
msgid "ClusterIntegration|Send Container Network Policies Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Send ModSecurity Logs"
|
||||
msgid "ClusterIntegration|Send Web Application Firewall Logs"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Service Token"
|
||||
|
|
@ -5575,6 +5581,15 @@ msgstr ""
|
|||
msgid "Code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Coverage: %{coveragePercentage}%{percentSymbol}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Coverage| Empty code coverage data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Coverage|Couldn't fetch the code coverage data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Owners"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12512,6 +12527,9 @@ msgstr ""
|
|||
msgid "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project."
|
||||
msgstr ""
|
||||
|
||||
msgid "It seems that there is currently no available data for code coverage"
|
||||
msgstr ""
|
||||
|
||||
msgid "It's you"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20950,6 +20968,9 @@ msgstr ""
|
|||
msgid "Something went wrong while updating a requirement."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while updating assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while updating your list settings"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24059,6 +24080,9 @@ msgstr ""
|
|||
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unassign from commenting user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unblock"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -25317,6 +25341,9 @@ msgstr ""
|
|||
msgid "We've found no vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
msgid "Web Application Firewall"
|
||||
msgstr ""
|
||||
|
||||
msgid "Web IDE"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26707,6 +26734,9 @@ msgstr ""
|
|||
msgid "customize"
|
||||
msgstr ""
|
||||
|
||||
msgid "data"
|
||||
msgstr ""
|
||||
|
||||
msgid "date must not be after 9999-12-31"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ describe('FluentdOutputSettings', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
desc | changeFn | key | value
|
||||
${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
|
||||
${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
|
||||
${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
|
||||
${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send ModSecurity Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
|
||||
${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Cilium Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
|
||||
desc | changeFn | key | value
|
||||
${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
|
||||
${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
|
||||
${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
|
||||
${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Web Application Firewall Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
|
||||
${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Container Network Policies Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
|
||||
`('$desc', ({ changeFn, key, value }) => {
|
||||
beforeEach(() => {
|
||||
changeFn();
|
||||
|
|
|
|||
|
|
@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants';
|
|||
import createStore from '~/notes/stores';
|
||||
import noteActions from '~/notes/components/note_actions.vue';
|
||||
import { userDataMock } from '../mock_data';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
describe('noteActions', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let props;
|
||||
let actions;
|
||||
let axiosMock;
|
||||
|
||||
const shallowMountNoteActions = propsData => {
|
||||
const shallowMountNoteActions = (propsData, computed) => {
|
||||
const localVue = createLocalVue();
|
||||
return shallowMount(localVue.extend(noteActions), {
|
||||
store,
|
||||
propsData,
|
||||
localVue,
|
||||
computed,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
props = {
|
||||
accessLevel: 'Maintainer',
|
||||
authorId: 26,
|
||||
authorId: 1,
|
||||
author: userDataMock,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
canAwardEmoji: true,
|
||||
|
|
@ -33,10 +40,17 @@ describe('noteActions', () => {
|
|||
reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
|
||||
showReply: false,
|
||||
};
|
||||
|
||||
actions = {
|
||||
updateAssignees: jest.fn(),
|
||||
};
|
||||
|
||||
axiosMock = new AxiosMockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
describe('user is logged in', () => {
|
||||
|
|
@ -76,6 +90,14 @@ describe('noteActions', () => {
|
|||
it('should not show copy link action when `noteUrl` prop is empty', done => {
|
||||
wrapper.setProps({
|
||||
...props,
|
||||
author: {
|
||||
avatar_url: 'mock_path',
|
||||
id: 26,
|
||||
name: 'Example Maintainer',
|
||||
path: '/ExampleMaintainer',
|
||||
state: 'active',
|
||||
username: 'ExampleMaintainer',
|
||||
},
|
||||
noteUrl: '',
|
||||
});
|
||||
|
||||
|
|
@ -104,6 +126,25 @@ describe('noteActions', () => {
|
|||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should be possible to assign or unassign the comment author', () => {
|
||||
wrapper = shallowMountNoteActions(props, {
|
||||
targetType: () => 'issue',
|
||||
});
|
||||
|
||||
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
|
||||
expect(assignUserButton.exists()).toBe(true);
|
||||
|
||||
assignUserButton.trigger('click');
|
||||
axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => {
|
||||
expect(actions.updateAssignees).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be possible to assign or unassign the comment author in a merge request', () => {
|
||||
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
|
||||
expect(assignUserButton.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1141,4 +1141,17 @@ describe('Actions Notes Store', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAssignees', () => {
|
||||
it('update the assignees state', done => {
|
||||
testAction(
|
||||
actions.updateAssignees,
|
||||
[userDataMock.id],
|
||||
{ state: noteableDataMock },
|
||||
[{ type: mutationTypes.UPDATE_ASSIGNEES, payload: [userDataMock.id] }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -805,4 +805,16 @@ describe('Notes Store mutations', () => {
|
|||
expect(state.batchSuggestionsInfo.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_ASSIGNEES', () => {
|
||||
it('should update assignees', () => {
|
||||
const state = {
|
||||
noteableData: noteableDataMock,
|
||||
};
|
||||
|
||||
mutations.UPDATE_ASSIGNEES(state, [userDataMock.id]);
|
||||
|
||||
expect(state.noteableData.assignees).toEqual([userDataMock.id]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="gl-mt-3 gl-mb-3"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<gl-dropdown-stub
|
||||
text="rspec"
|
||||
>
|
||||
<gl-dropdown-item-stub
|
||||
value="rspec"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="gl-absolute"
|
||||
name="mobile-issue-close"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="gl-display-flex align-items-center ml-4"
|
||||
>
|
||||
|
||||
rspec
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</gl-dropdown-item-stub>
|
||||
<gl-dropdown-item-stub
|
||||
value="cypress"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-display-flex align-items-center ml-4"
|
||||
>
|
||||
|
||||
cypress
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</gl-dropdown-item-stub>
|
||||
<gl-dropdown-item-stub
|
||||
value="karma"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-display-flex align-items-center ml-4"
|
||||
>
|
||||
|
||||
karma
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</gl-dropdown-item-stub>
|
||||
</gl-dropdown-stub>
|
||||
</div>
|
||||
|
||||
<gl-area-chart-stub
|
||||
annotations=""
|
||||
data="[object Object]"
|
||||
formattooltiptext="function () { [native code] }"
|
||||
height="200"
|
||||
includelegendavgmax="true"
|
||||
legendaveragetext="Avg"
|
||||
legendcurrenttext="Current"
|
||||
legendlayout="inline"
|
||||
legendmaxtext="Max"
|
||||
legendmintext="Min"
|
||||
option="[object Object]"
|
||||
thresholds=""
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAlert, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { GlAreaChart } from '@gitlab/ui/dist/charts';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
|
||||
import codeCoverageMockData from './mock_data';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
|
||||
describe('Code Coverage', () => {
|
||||
let wrapper;
|
||||
let mockAxios;
|
||||
|
||||
const graphEndpoint = '/graph';
|
||||
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findAreaChart = () => wrapper.find(GlAreaChart);
|
||||
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
|
||||
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
|
||||
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(CodeCoverage, {
|
||||
propsData: {
|
||||
graphEndpoint,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('when fetching data is successful', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
|
||||
|
||||
createComponent();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
it('renders the area chart', () => {
|
||||
expect(findAreaChart().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('matches the snapshot', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows no error messages', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetching data fails', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet().replyOnce(httpStatusCodes.BAD_REQUEST);
|
||||
|
||||
createComponent();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
it('renders an error message', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().attributes().variant).toBe('danger');
|
||||
});
|
||||
|
||||
it('still renders an empty graph', () => {
|
||||
expect(findAreaChart().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when fetching data succeed but returns an empty state', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet().replyOnce(httpStatusCodes.OK, []);
|
||||
|
||||
createComponent();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
it('renders an information message', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().attributes().variant).toBe('info');
|
||||
});
|
||||
|
||||
it('still renders an empty graph', () => {
|
||||
expect(findAreaChart().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropdown options', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
|
||||
|
||||
createComponent();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('renders the dropdown with all custom names as options', () => {
|
||||
expect(wrapper.contains(GlDropdown)).toBeDefined();
|
||||
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
|
||||
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
|
||||
|
||||
createComponent();
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('updates the selected dropdown option with an icon', async () => {
|
||||
findSecondDropdownItem().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
findFirstDropdownItem()
|
||||
.find(GlIcon)
|
||||
.exists(),
|
||||
).toBe(false);
|
||||
expect(findSecondDropdownItem().contains(GlIcon)).toBe(true);
|
||||
});
|
||||
|
||||
it('updates the graph data when selecting a different option in dropdown', async () => {
|
||||
const originalSelectedData = wrapper.vm.selectedDailyCoverage;
|
||||
const expectedData = codeCoverageMockData[1];
|
||||
|
||||
findSecondDropdownItem().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.selectedDailyCoverage).not.toBe(originalSelectedData);
|
||||
expect(wrapper.vm.selectedDailyCoverage).toBe(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
export default [
|
||||
{
|
||||
group_name: 'rspec',
|
||||
data: [
|
||||
{ date: '2020-04-30', coverage: 40.0 },
|
||||
{ date: '2020-05-01', coverage: 80.0 },
|
||||
{ date: '2020-05-02', coverage: 99.0 },
|
||||
{ date: '2020-05-10', coverage: 80.0 },
|
||||
{ date: '2020-05-15', coverage: 70.0 },
|
||||
{ date: '2020-05-20', coverage: 69.0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
group_name: 'cypress',
|
||||
data: [
|
||||
{ date: '2022-07-30', coverage: 1.0 },
|
||||
{ date: '2022-08-01', coverage: 2.4 },
|
||||
{ date: '2022-08-02', coverage: 5.0 },
|
||||
{ date: '2022-08-10', coverage: 15.0 },
|
||||
{ date: '2022-08-15', coverage: 30.0 },
|
||||
{ date: '2022-08-20', coverage: 40.0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
group_name: 'karma',
|
||||
data: [
|
||||
{ date: '2020-05-01', coverage: 94.0 },
|
||||
{ date: '2020-05-02', coverage: 94.0 },
|
||||
{ date: '2020-05-03', coverage: 94.0 },
|
||||
{ date: '2020-05-04', coverage: 94.0 },
|
||||
{ date: '2020-05-05', coverage: 92.0 },
|
||||
{ date: '2020-05-06', coverage: 91.0 },
|
||||
{ date: '2020-05-07', coverage: 78.0 },
|
||||
{ date: '2020-05-08', coverage: 94.0 },
|
||||
{ date: '2020-05-09', coverage: 94.0 },
|
||||
{ date: '2020-05-10', coverage: 94.0 },
|
||||
{ date: '2020-05-11', coverage: 94.0 },
|
||||
{ date: '2020-05-12', coverage: 94.0 },
|
||||
{ date: '2020-05-13', coverage: 92.0 },
|
||||
{ date: '2020-05-14', coverage: 91.0 },
|
||||
{ date: '2020-05-15', coverage: 78.0 },
|
||||
{ date: '2020-05-16', coverage: 94.0 },
|
||||
{ date: '2020-05-17', coverage: 94.0 },
|
||||
{ date: '2020-05-18', coverage: 93.0 },
|
||||
{ date: '2020-05-19', coverage: 92.0 },
|
||||
{ date: '2020-05-20', coverage: 91.0 },
|
||||
{ date: '2020-05-21', coverage: 90.0 },
|
||||
{ date: '2020-05-22', coverage: 91.0 },
|
||||
{ date: '2020-05-23', coverage: 92.0 },
|
||||
{ date: '2020-05-24', coverage: 75.0 },
|
||||
{ date: '2020-05-25', coverage: 74.0 },
|
||||
{ date: '2020-05-26', coverage: 74.0 },
|
||||
{ date: '2020-05-27', coverage: 74.0 },
|
||||
{ date: '2020-05-28', coverage: 80.0 },
|
||||
{ date: '2020-05-29', coverage: 85.0 },
|
||||
{ date: '2020-05-30', coverage: 92.0 },
|
||||
{ date: '2020-05-31', coverage: 91.0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -33,7 +33,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
|
|||
let(:nodes) { Project.order(:updated_at) }
|
||||
|
||||
it 'returns the encoded value of the order' do
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
|
||||
end
|
||||
|
||||
it 'includes the :id even when not specified in the order' do
|
||||
|
|
@ -45,7 +45,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
|
|||
let(:nodes) { Project.order(:updated_at).order(:created_at) }
|
||||
|
||||
it 'returns the encoded value of the order' do
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
|
|||
let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) }
|
||||
|
||||
it 'returns the encoded value of the order' do
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
|
||||
expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'migrate', '20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb')
|
||||
|
||||
describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
let(:approval_rules) { table(:approval_merge_request_rules) }
|
||||
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
|
||||
|
||||
let(:project) do
|
||||
table(:projects).create!(
|
||||
namespace_id: namespace.id,
|
||||
name: 'gitlab',
|
||||
path: 'gitlab'
|
||||
)
|
||||
end
|
||||
|
||||
let(:merge_request) do
|
||||
table(:merge_requests).create!(
|
||||
target_project_id: project.id,
|
||||
source_project_id: project.id,
|
||||
target_branch: 'feature',
|
||||
source_branch: 'master'
|
||||
)
|
||||
end
|
||||
|
||||
let(:index_names) do
|
||||
ActiveRecord::Base.connection
|
||||
.indexes(:approval_merge_request_rules)
|
||||
.collect(&:name)
|
||||
end
|
||||
|
||||
def create_sectional_approval_rules
|
||||
approval_rules.create!(
|
||||
merge_request_id: merge_request.id,
|
||||
name: "*.rb",
|
||||
code_owner: true,
|
||||
rule_type: 2,
|
||||
section: "First Section"
|
||||
)
|
||||
|
||||
approval_rules.create!(
|
||||
merge_request_id: merge_request.id,
|
||||
name: "*.rb",
|
||||
code_owner: true,
|
||||
rule_type: 2,
|
||||
section: "Second Section"
|
||||
)
|
||||
end
|
||||
|
||||
def create_two_matching_nil_section_approval_rules
|
||||
2.times do
|
||||
approval_rules.create!(
|
||||
merge_request_id: merge_request.id,
|
||||
name: "nil_section",
|
||||
code_owner: true,
|
||||
rule_type: 2
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
approval_rules.delete_all
|
||||
end
|
||||
|
||||
describe "#up" do
|
||||
it "creates the new index and removes the 'legacy' indices" do
|
||||
# Confirm that existing legacy indices prevent duplicate entries
|
||||
#
|
||||
expect { create_sectional_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
expect { create_two_matching_nil_section_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
|
||||
approval_rules.delete_all
|
||||
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
# After running the migration, expect `section == nil` rules to still
|
||||
# be blocked by the legacy indices, but sectional rules are allowed.
|
||||
#
|
||||
expect { create_sectional_approval_rules }
|
||||
.to change { approval_rules.count }.by(2)
|
||||
expect { create_two_matching_nil_section_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
|
||||
# Attempt to rerun the creation of sectional rules, and see that sectional
|
||||
# rules are unique by section
|
||||
#
|
||||
expect { create_sectional_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
|
||||
expect(index_names).to include(
|
||||
described_class::SECTIONAL_INDEX_NAME,
|
||||
described_class::LEGACY_INDEX_NAME_RULE_TYPE,
|
||||
described_class::LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#down" do
|
||||
it "recreates 'legacy' indices and removes duplicate code owner approval rules" do
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
expect { create_sectional_approval_rules }
|
||||
.to change { approval_rules.count }.by(2)
|
||||
expect { create_two_matching_nil_section_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
|
||||
# Run the down migration. This will remove the 2 approval rules we create
|
||||
# above, and call MergeRequests::SyncCodeOwnerApprovalRules to recreate
|
||||
# new ones.
|
||||
#
|
||||
expect(MergeRequests::SyncCodeOwnerApprovalRules)
|
||||
.to receive(:new).with(MergeRequest.find(merge_request.id)).once.and_call_original
|
||||
|
||||
# We expect approval_rules.count to be changed by -2 as we're deleting the
|
||||
# 3 rules created above, and MergeRequests::SyncCodeOwnerApprovalRules
|
||||
# will not be able to create new one with an empty (or missing)
|
||||
# CODEOWNERS file.
|
||||
#
|
||||
expect { disable_migrations_output { migration.down } }
|
||||
.to change { approval_rules.count }.by(-3)
|
||||
|
||||
# Test that the index does not allow us to create the same rules as the
|
||||
# previous sectional index.
|
||||
#
|
||||
expect { create_sectional_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
expect { create_two_matching_nil_section_approval_rules }
|
||||
.to raise_exception(ActiveRecord::RecordNotUnique)
|
||||
|
||||
expect(index_names).not_to include(described_class::SECTIONAL_INDEX_NAME)
|
||||
expect(index_names).to include(
|
||||
described_class::LEGACY_INDEX_NAME_RULE_TYPE,
|
||||
described_class::LEGACY_INDEX_NAME_CODE_OWNERS
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -187,4 +187,62 @@ describe 'GraphQL' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'keyset pagination' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) }
|
||||
|
||||
let(:page_size) { 6 }
|
||||
let(:issues_edges) { %w(data project issues edges) }
|
||||
let(:end_cursor) { %w(data project issues pageInfo endCursor) }
|
||||
let(:query) do
|
||||
<<~GRAPHQL
|
||||
query project($fullPath: ID!, $first: Int, $after: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
issues(first: $first, after: $after) {
|
||||
edges { node { iid } }
|
||||
pageInfo { endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
# TODO: Switch this to use `post_graphql`
|
||||
# This is not performing an actual GraphQL request because the
|
||||
# variables end up being strings when passed through the `post_graphql`
|
||||
# helper.
|
||||
#
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/222432
|
||||
def execute_query(after: nil)
|
||||
GitlabSchema.execute(
|
||||
query,
|
||||
context: { current_user: nil },
|
||||
variables: {
|
||||
fullPath: project.full_path,
|
||||
first: page_size,
|
||||
after: after
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'paginates datetimes correctly when they have millisecond data' do
|
||||
# let's make sure we're actually querying a timestamp, just in case
|
||||
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
|
||||
.to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
|
||||
|
||||
first_page = execute_query
|
||||
edges = first_page.dig(*issues_edges)
|
||||
cursor = first_page.dig(*end_cursor)
|
||||
|
||||
expect(edges.count).to eq(6)
|
||||
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
|
||||
|
||||
second_page = execute_query(after: cursor)
|
||||
edges = second_page.dig(*issues_edges)
|
||||
|
||||
expect(edges.count).to eq(4)
|
||||
expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ describe AlertManagement::Alerts::UpdateService do
|
|||
describe '#execute' do
|
||||
subject(:response) { service.execute }
|
||||
|
||||
context 'when the current_user is nil' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it 'results in an error' do
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq('You have no permissions')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission to update alerts' do
|
||||
let(:current_user) { user_without_permissions }
|
||||
|
||||
|
|
@ -81,6 +90,37 @@ describe AlertManagement::Alerts::UpdateService do
|
|||
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
|
||||
end
|
||||
|
||||
context 'when current user is not the assignee' do
|
||||
let(:assignee_user) { create(:user) }
|
||||
let(:params) { { assignees: [assignee_user] } }
|
||||
|
||||
it 'skips adding todo for assignee without permission to read alert' do
|
||||
expect { response }.not_to change(Todo, :count)
|
||||
end
|
||||
|
||||
context 'when assignee has read permission' do
|
||||
before do
|
||||
project.add_developer(assignee_user)
|
||||
end
|
||||
|
||||
it 'adds a todo' do
|
||||
response
|
||||
|
||||
expect(Todo.first.author).to eq(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current_user is nil' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it 'skips adding todo if current_user is nil' do
|
||||
project.add_developer(assignee_user)
|
||||
|
||||
expect { response }.not_to change(Todo, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple users included' do
|
||||
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue