Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9919ffe0c8
commit
6a6e9bec88
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import ReportHeader from './report_header.vue';
|
||||
import UserDetails from './user_details.vue';
|
||||
import ReportDetails from './report_details.vue';
|
||||
import ReportedContent from './reported_content.vue';
|
||||
import HistoryItems from './history_items.vue';
|
||||
|
||||
|
|
@ -17,9 +19,11 @@ export default {
|
|||
GlAlert,
|
||||
ReportHeader,
|
||||
UserDetails,
|
||||
ReportDetails,
|
||||
ReportedContent,
|
||||
HistoryItems,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
abuseReport: {
|
||||
type: Object,
|
||||
|
|
@ -63,6 +67,12 @@ export default {
|
|||
/>
|
||||
<user-details v-if="abuseReport.user" :user="abuseReport.user" />
|
||||
|
||||
<report-details
|
||||
v-if="glFeatures.abuseReportLabels"
|
||||
:report-id="abuseReport.report.globalId"
|
||||
class="gl-mt-6"
|
||||
/>
|
||||
|
||||
<reported-content :report="abuseReport.report" data-testid="reported-content" />
|
||||
|
||||
<div v-for="report in similarOpenReports" :key="report.id" data-testid="similar-open-reports">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
query abuseReportQuery($id: AbuseReportID!) {
|
||||
abuseReport(id: $id) {
|
||||
labels {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
description
|
||||
color
|
||||
textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
query abuseReportLabelsQuery($searchTerm: String) {
|
||||
abuseReportLabels(searchTerm: $searchTerm) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
description
|
||||
color
|
||||
textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
<script>
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { createAlert } from '~/alert';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
|
||||
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
|
||||
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
|
||||
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
|
||||
import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownWidget,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
LabelItem,
|
||||
DropdownValue,
|
||||
DropdownHeader,
|
||||
},
|
||||
inject: ['updatePath'],
|
||||
props: {
|
||||
report: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
labels: [],
|
||||
selected: this.report.labels,
|
||||
initialLoading: true,
|
||||
isEditing: false,
|
||||
isUpdating: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
labels: {
|
||||
query() {
|
||||
return abuseReportLabelsQuery;
|
||||
},
|
||||
variables() {
|
||||
return { searchTerm: this.search };
|
||||
},
|
||||
skip() {
|
||||
return !this.isEditing;
|
||||
},
|
||||
update(data) {
|
||||
return data.abuseReportLabels?.nodes;
|
||||
},
|
||||
error() {
|
||||
createAlert({ message: this.$options.i18n.searchError });
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLabelsEmpty() {
|
||||
return this.selected.length === 0;
|
||||
},
|
||||
selectedLabelIds() {
|
||||
return this.selected.map((label) => label.id);
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.labels.loading;
|
||||
},
|
||||
selectText() {
|
||||
if (!this.selected.length) {
|
||||
return this.$options.i18n.labelsListTitle;
|
||||
} else if (this.selected.length > 1) {
|
||||
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
|
||||
firstLabelName: this.selected[0].title,
|
||||
remainingLabelCount: this.selected.length - 1,
|
||||
});
|
||||
}
|
||||
return this.selected[0].title;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
report({ labels }) {
|
||||
this.selected = labels;
|
||||
this.initialLoading = false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const setSearch = (search) => {
|
||||
this.search = search;
|
||||
};
|
||||
this.debouncedSetSearch = debounce(setSearch, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
methods: {
|
||||
toggleEdit() {
|
||||
return this.isEditing ? this.hideDropdown() : this.showDropdown();
|
||||
},
|
||||
showDropdown() {
|
||||
this.isEditing = true;
|
||||
this.$refs.editDropdown.showDropdown();
|
||||
},
|
||||
hideDropdown() {
|
||||
this.saveSelectedLabels();
|
||||
this.isEditing = false;
|
||||
},
|
||||
saveSelectedLabels() {
|
||||
this.isUpdating = true;
|
||||
|
||||
axios
|
||||
.put(this.updatePath, { label_ids: this.selectedLabelIds })
|
||||
.catch((error) => {
|
||||
createAlert({
|
||||
message: __('An error occurred while updating labels.'),
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
isLabelSelected(label) {
|
||||
return this.selectedLabelIds.includes(label.id);
|
||||
},
|
||||
filterSelected(id) {
|
||||
return this.selected.filter(({ id: labelId }) => labelId !== id);
|
||||
},
|
||||
toggleLabelSelection(label) {
|
||||
this.selected = this.isLabelSelected(label)
|
||||
? this.filterSelected(label.id)
|
||||
: [...this.selected, label];
|
||||
},
|
||||
removeLabel(labelId) {
|
||||
this.selected = this.filterSelected(labelId);
|
||||
this.saveSelectedLabels();
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
label: __('Labels'),
|
||||
noLabels: __('None'),
|
||||
labelsListTitle: __('Assign labels'),
|
||||
searchError: __('An error occurred while searching for labels, please try again.'),
|
||||
edit: __('Edit'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="labels-select-wrapper">
|
||||
<div class="gl-display-flex gl-align-items-center gl-gap-3 gl-mb-2">
|
||||
<span>{{ $options.i18n.label }}</span>
|
||||
<gl-loading-icon v-if="initialLoading" size="sm" inline class="gl-ml-2" />
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
size="small"
|
||||
:disabled="isUpdating || initialLoading"
|
||||
class="edit-link gl-ml-auto"
|
||||
@click="toggleEdit"
|
||||
>
|
||||
{{ $options.i18n.edit }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div class="gl-text-gray-500 gl-mb-2" data-testid="selected-labels">
|
||||
<template v-if="isLabelsEmpty">{{ $options.i18n.noLabels }}</template>
|
||||
<dropdown-value
|
||||
v-else
|
||||
:disable-labels="isLoading"
|
||||
:selected-labels="selected"
|
||||
:allow-label-remove="!isUpdating"
|
||||
:labels-filter-base-path="''"
|
||||
:labels-filter-param="'label_name'"
|
||||
@onLabelRemove="removeLabel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<dropdown-widget
|
||||
v-show="isEditing"
|
||||
ref="editDropdown"
|
||||
:select-text="selectText"
|
||||
:options="labels"
|
||||
:is-loading="isLoading"
|
||||
:selected="selected"
|
||||
:search-term="search"
|
||||
:allow-multiselect="true"
|
||||
@hide="hideDropdown"
|
||||
@set-option="toggleLabelSelection"
|
||||
@set-search="debouncedSetSearch"
|
||||
>
|
||||
<template #header>
|
||||
<dropdown-header
|
||||
ref="header"
|
||||
:search-key="search"
|
||||
labels-create-title=""
|
||||
:labels-list-title="$options.i18n.labelsListTitle"
|
||||
:show-dropdown-contents-create-view="false"
|
||||
@closeDropdown="hideDropdown"
|
||||
@input="debouncedSetSearch"
|
||||
/>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<label-item :label="item" />
|
||||
</template>
|
||||
</dropdown-widget>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import { createAlert } from '~/alert';
|
||||
import LabelsSelect from './labels_select.vue';
|
||||
import abuseReportQuery from './graphql/abuse_report.query.graphql';
|
||||
|
||||
export default {
|
||||
name: 'ReportDetails',
|
||||
components: {
|
||||
LabelsSelect,
|
||||
},
|
||||
props: {
|
||||
reportId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
report: { labels: [] },
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
report: {
|
||||
query() {
|
||||
return abuseReportQuery;
|
||||
},
|
||||
variables() {
|
||||
return { id: this.reportId };
|
||||
},
|
||||
update({ abuseReport }) {
|
||||
return {
|
||||
labels: abuseReport.labels?.nodes,
|
||||
};
|
||||
},
|
||||
error() {
|
||||
createAlert({ message: this.$options.i18n.fetchError });
|
||||
},
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
fetchError: __('An error occurred while fetching labels, please try again.'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<labels-select :report="report" />
|
||||
</template>
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { defaultClient } from '~/graphql_shared/issuable_client';
|
||||
import AbuseReportApp from './components/abuse_report_app.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient,
|
||||
});
|
||||
|
||||
export const initAbuseReportApp = () => {
|
||||
const el = document.querySelector('#js-abuse-reports-detail-view');
|
||||
|
||||
|
|
@ -16,7 +24,12 @@ export const initAbuseReportApp = () => {
|
|||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
name: 'AbuseReportAppRoot',
|
||||
provide: {
|
||||
allowScopedLabels: false,
|
||||
updatePath: abuseReport.report.updatePath,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement(AbuseReportApp, {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ export default {
|
|||
:initial-filter-value="initialFilterValue"
|
||||
:tokens="validTokens"
|
||||
:initial-sort-by="initialSortBy"
|
||||
:search-input-placeholder="__('Search or filter results...')"
|
||||
:search-text-option-label="s__('Runners|Search description...')"
|
||||
terms-as-tokens
|
||||
data-testid="runners-filtered-search"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default class FilteredSearchManager {
|
|||
useDefaultState = false,
|
||||
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
|
||||
stateFiltersSelector = '.issues-state-filters',
|
||||
placeholder = __('Search or filter results...'),
|
||||
placeholder = __('Search or filter results…'),
|
||||
anchor = null,
|
||||
}) {
|
||||
this.isGroup = isGroup;
|
||||
|
|
|
|||
|
|
@ -495,7 +495,6 @@ export default {
|
|||
:issuables-loading="isLoading"
|
||||
namespace="dashboard"
|
||||
recent-searches-storage-key="issues"
|
||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||
:search-tokens="searchTokens"
|
||||
:show-pagination-controls="showPaginationControls"
|
||||
show-work-item-type-icon
|
||||
|
|
|
|||
|
|
@ -966,7 +966,6 @@ export default {
|
|||
v-if="hasAnyIssues"
|
||||
:namespace="fullPath"
|
||||
recent-searches-storage-key="issues"
|
||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||
:search-tokens="searchTokens"
|
||||
:has-scoped-labels-feature="hasScopedLabelsFeature"
|
||||
:initial-filter-value="filterTokens"
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ export const i18n = {
|
|||
reorderError: __('An error occurred while reordering issues.'),
|
||||
deleteError: __('An error occurred while deleting an issuable.'),
|
||||
rssLabel: __('Subscribe to RSS feed'),
|
||||
searchPlaceholder: __('Search or filter results...'),
|
||||
upvotes: __('Upvotes'),
|
||||
titles: __('Titles'),
|
||||
descriptions: __('Descriptions'),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ import reorderServiceDeskIssuesMutation from '../queries/reorder_service_desk_is
|
|||
import {
|
||||
errorFetchingCounts,
|
||||
errorFetchingIssues,
|
||||
searchPlaceholder,
|
||||
issueRepositioningMessage,
|
||||
reorderError,
|
||||
SERVICE_DESK_BOT_USERNAME,
|
||||
|
|
@ -77,7 +76,6 @@ export default {
|
|||
i18n: {
|
||||
errorFetchingCounts,
|
||||
errorFetchingIssues,
|
||||
searchPlaceholder,
|
||||
issueRepositioningMessage,
|
||||
reorderError,
|
||||
},
|
||||
|
|
@ -559,7 +557,6 @@ export default {
|
|||
namespace="service-desk"
|
||||
recent-searches-storage-key="service-desk-issues"
|
||||
:error="issuesError"
|
||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||
:search-tokens="searchTokens"
|
||||
:issuables-loading="isLoading"
|
||||
:initial-filter-value="filterTokens"
|
||||
|
|
|
|||
|
|
@ -235,7 +235,6 @@ export const noSearchResultsDescription = __(
|
|||
'To widen your search, change or remove filters above',
|
||||
);
|
||||
export const noSearchResultsTitle = __('Sorry, your filter produced no results');
|
||||
export const searchPlaceholder = __('Search or filter results...');
|
||||
export const issueRepositioningMessage = __(
|
||||
'Issues are being rebalanced at the moment, so manual reordering is disabled.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import { __ } from './locale';
|
||||
|
||||
/**
|
||||
|
|
@ -47,6 +48,15 @@ export function toggleSection($section) {
|
|||
}
|
||||
}
|
||||
|
||||
export function initTrackProductAnalyticsExpanded() {
|
||||
const $analyticsSection = $('#js-product-analytics-settings');
|
||||
$analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => {
|
||||
if (isExpanded($analyticsSection)) {
|
||||
InternalEvents.track_event('user_viewed_cluster_configuration');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function initSettingsPanels() {
|
||||
$('.settings').each((i, elm) => {
|
||||
const $section = $(elm);
|
||||
|
|
@ -64,4 +74,6 @@ export default function initSettingsPanels() {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
initTrackProductAnalyticsExpanded();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,8 @@ export default {
|
|||
},
|
||||
searchInputPlaceholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: __('Search or filter results…'),
|
||||
},
|
||||
suggestionsListClass: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const tdClass =
|
||||
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
|
||||
export const thClass = 'gl-hover-bg-blue-50';
|
||||
|
|
@ -15,7 +13,3 @@ export const initialPaginationState = {
|
|||
firstPageSize: defaultPageSize,
|
||||
lastPageSize: null,
|
||||
};
|
||||
|
||||
export const defaultI18n = {
|
||||
searchPlaceholder: __('Search or filter results…'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@ import {
|
|||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
|
||||
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
|
||||
import { initialPaginationState, defaultPageSize } from './constants';
|
||||
import { isAny } from './utils';
|
||||
|
||||
export default {
|
||||
defaultI18n,
|
||||
components: {
|
||||
GlAlert,
|
||||
GlBadge,
|
||||
|
|
@ -300,7 +299,6 @@ export default {
|
|||
<div class="filtered-search-wrapper">
|
||||
<filtered-search-bar
|
||||
:namespace="projectPath"
|
||||
:search-input-placeholder="$options.defaultI18n.searchPlaceholder"
|
||||
:tokens="filteredSearchTokens"
|
||||
:initial-filter-value="filteredSearchValue"
|
||||
initial-sortby="created_desc"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
|||
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
|
|
@ -51,7 +52,8 @@ export default {
|
|||
},
|
||||
searchInputPlaceholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: __('Search or filter results…'),
|
||||
},
|
||||
searchTokens: {
|
||||
type: Array,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export default {
|
|||
showSuperSidebarToggle() {
|
||||
return gon.use_new_navigation && sidebarState.isCollapsed;
|
||||
},
|
||||
|
||||
topBarClasses() {
|
||||
return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
|
@ -120,15 +124,17 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
|
||||
>
|
||||
<super-sidebar-toggle
|
||||
v-if="showSuperSidebarToggle"
|
||||
class="gl-mr-2"
|
||||
:class="$options.JS_TOGGLE_EXPAND_CLASS"
|
||||
/>
|
||||
<gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
|
||||
<div :class="topBarClasses" data-testid="top-bar">
|
||||
<div
|
||||
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
|
||||
>
|
||||
<super-sidebar-toggle
|
||||
v-if="showSuperSidebarToggle"
|
||||
class="gl-mr-2"
|
||||
:class="$options.JS_TOGGLE_EXPAND_CLASS"
|
||||
/>
|
||||
<gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activePanel">
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ import { STATE_CLOSED } from '../../constants';
|
|||
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
searchPlaceholder: __('Search or filter results...'),
|
||||
},
|
||||
issuableListTabs,
|
||||
components: {
|
||||
IssuableList,
|
||||
|
|
@ -64,7 +61,6 @@ export default {
|
|||
:issuables-loading="$apollo.queries.workItems.loading"
|
||||
namespace="work-items"
|
||||
recent-searches-storage-key="issues"
|
||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||
:search-tokens="searchTokens"
|
||||
show-work-item-type-icon
|
||||
:sort-options="sortOptions"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ class Admin::AbuseReportsController < Admin::ApplicationController
|
|||
feature_category :insider_threat
|
||||
|
||||
before_action :set_status_param, only: :index, if: -> { Feature.enabled?(:abuse_reports_list) }
|
||||
before_action :find_abuse_report, only: [:show, :moderate_user, :update, :destroy]
|
||||
before_action :find_abuse_report, only: [:show, :update, :moderate_user, :destroy]
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:abuse_report_labels)
|
||||
end
|
||||
|
||||
def index
|
||||
@abuse_reports = AbuseReportsFinder.new(params).execute
|
||||
|
|
@ -12,14 +15,11 @@ class Admin::AbuseReportsController < Admin::ApplicationController
|
|||
|
||||
def show; end
|
||||
|
||||
# Kept for backwards compatibility.
|
||||
# TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
|
||||
# In 16.4 remove or re-use this endpoint after frontend has migrated to using moderate_user endpoint
|
||||
def update
|
||||
response = Admin::AbuseReports::ModerateUserService.new(@abuse_report, current_user, permitted_params).execute
|
||||
response = Admin::AbuseReports::UpdateService.new(@abuse_report, current_user, permitted_params).execute
|
||||
|
||||
if response.success?
|
||||
render json: { message: response.message }
|
||||
head :ok
|
||||
else
|
||||
render json: { message: response.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
|
@ -53,6 +53,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:user_action, :close, :reason, :comment)
|
||||
params.permit(:user_action, :close, :reason, :comment, { label_ids: [] })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
|
|||
service_desk_settings = project.service_desk_setting
|
||||
|
||||
{
|
||||
service_desk_address: project.service_desk_address,
|
||||
service_desk_address: project.service_desk_system_address,
|
||||
service_desk_enabled: project.service_desk_enabled,
|
||||
issue_template_key: service_desk_settings&.issue_template_key,
|
||||
template_file_missing: service_desk_settings&.issue_template_missing?,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ module Resolvers
|
|||
alias_method :runner, :object
|
||||
|
||||
def resolve_with_lookahead(statuses: nil)
|
||||
context[:job_field_authorization] = :read_build # Instruct JobType to perform field-level authorization
|
||||
|
||||
jobs = ::Ci::JobsFinder.new(current_user: current_user, runner: runner, params: { scope: statuses }).execute
|
||||
|
||||
apply_lookahead(jobs)
|
||||
|
|
@ -30,7 +32,7 @@ module Resolvers
|
|||
previous_stage_jobs_or_needs: [:needs, :pipeline],
|
||||
artifacts: [:job_artifacts],
|
||||
pipeline: [:user],
|
||||
project: [{ project: [:route, { namespace: [:route] }] }],
|
||||
project: [{ project: [:route, { namespace: [:route] }, :project_feature] }],
|
||||
detailed_status: [
|
||||
:metadata,
|
||||
{ pipeline: [:merge_request] },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
# JobBaseField ensures that only allow-listed fields can be returned without a permission check.
|
||||
# All other fields go through a permissions check based on the :job_field_authorization value passed in the context.
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class JobBaseField < ::Types::BaseField
|
||||
PUBLIC_FIELDS = %i[allow_failure duration id kind status created_at finished_at queued_at queued_duration
|
||||
updated_at runner short_sha].freeze
|
||||
|
||||
def authorized?(object, args, ctx)
|
||||
current_user = ctx[:current_user]
|
||||
permission = ctx[:job_field_authorization]
|
||||
|
||||
if permission.nil? ||
|
||||
PUBLIC_FIELDS.include?(ctx[:current_field].original_name) ||
|
||||
current_user.can?(permission, object)
|
||||
return super
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,7 @@ module Types
|
|||
graphql_name 'CiJob'
|
||||
|
||||
present_using ::Ci::BuildPresenter
|
||||
field_class Types::Ci::JobBaseField
|
||||
|
||||
connection_type_class Types::LimitedCountableConnectionType
|
||||
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ module ApplicationHelper
|
|||
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
|
||||
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
|
||||
class_names << 'with-performance-bar' if performance_bar_enabled?
|
||||
class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar
|
||||
class_names << 'with-top-bar' if show_super_sidebar? && !@hide_top_bar_padding
|
||||
class_names << system_message_class
|
||||
class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
|
||||
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ module SearchHelper
|
|||
end.to_json
|
||||
end
|
||||
|
||||
def search_filter_input_options(type, placeholder = _('Search or filter results...'))
|
||||
def search_filter_input_options(type, placeholder = _('Search or filter results…'))
|
||||
opts =
|
||||
{
|
||||
id: "filtered-search-#{type}",
|
||||
|
|
|
|||
|
|
@ -2963,7 +2963,11 @@ class Project < ApplicationRecord
|
|||
alias_method :service_desk_enabled?, :service_desk_enabled
|
||||
|
||||
def service_desk_address
|
||||
service_desk_custom_address || service_desk_incoming_address
|
||||
service_desk_custom_address || service_desk_system_address
|
||||
end
|
||||
|
||||
def service_desk_system_address
|
||||
service_desk_alias_address || service_desk_incoming_address
|
||||
end
|
||||
|
||||
def service_desk_incoming_address
|
||||
|
|
@ -2975,7 +2979,7 @@ class Project < ApplicationRecord
|
|||
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
|
||||
end
|
||||
|
||||
def service_desk_custom_address
|
||||
def service_desk_alias_address
|
||||
return unless Gitlab::Email::ServiceDeskEmail.enabled?
|
||||
|
||||
key = service_desk_setting&.project_key || default_service_desk_suffix
|
||||
|
|
@ -2983,6 +2987,13 @@ class Project < ApplicationRecord
|
|||
Gitlab::Email::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
|
||||
end
|
||||
|
||||
def service_desk_custom_address
|
||||
return unless Feature.enabled?(:service_desk_custom_email, self)
|
||||
return unless service_desk_setting&.custom_email_enabled?
|
||||
|
||||
service_desk_setting.custom_email
|
||||
end
|
||||
|
||||
def default_service_desk_suffix
|
||||
"#{id}-issue-"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ module Admin
|
|||
include RequestAwareEntity
|
||||
|
||||
expose :id
|
||||
expose :global_id do |report|
|
||||
Gitlab::GlobalId.build(report, id: report.id).to_s
|
||||
end
|
||||
expose :status
|
||||
expose :message
|
||||
expose :created_at, as: :reported_at
|
||||
|
|
@ -24,9 +27,6 @@ module Admin
|
|||
end
|
||||
end
|
||||
|
||||
# Kept for backwards compatibility.
|
||||
# TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
|
||||
# In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
|
||||
expose :update_path do |report|
|
||||
admin_abuse_report_path(report)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module AbuseReports
|
||||
class UpdateService < BaseService
|
||||
attr_reader :abuse_report, :params, :current_user
|
||||
|
||||
def initialize(abuse_report, current_user, params)
|
||||
@abuse_report = abuse_report
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return ServiceResponse.error(message: 'Admin is required') unless current_user&.can_admin_all_resources?
|
||||
|
||||
abuse_report.label_ids = label_ids
|
||||
|
||||
ServiceResponse.success
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def label_ids
|
||||
params[:label_ids].filter_map do |id|
|
||||
GitlabSchema.parse_gid(id, expected_type: ::Admin::AbuseReportLabel).model_id
|
||||
rescue Gitlab::Graphql::Errors::ArgumentError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -44,6 +44,7 @@ module NotificationRecipients
|
|||
def add_recipients(users, type, reason)
|
||||
if users.is_a?(ActiveRecord::Relation)
|
||||
users = users.includes(:notification_settings)
|
||||
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/421821')
|
||||
end
|
||||
|
||||
users = Array(users).compact
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
!!! 5
|
||||
- add_page_specific_style 'page_bundles/terms'
|
||||
- @hide_top_bar = true
|
||||
- @hide_top_bar_padding = true
|
||||
- body_classes = [user_application_theme]
|
||||
%html{ lang: I18n.locale, class: page_class }
|
||||
= render "layouts/head"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
enabled: "#{@project.service_desk_enabled}",
|
||||
issue_tracker_enabled: "#{@project.project_feature.issues_enabled?}",
|
||||
incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
|
||||
service_desk_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
|
||||
service_desk_email: (@project.service_desk_alias_address if @project.service_desk_enabled),
|
||||
service_desk_email_enabled: "#{Gitlab::Email::ServiceDeskEmail.enabled?}",
|
||||
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
|
||||
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
- type = local_assigns.fetch(:type)
|
||||
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
|
||||
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
|
||||
- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
|
||||
- placeholder = local_assigns[:placeholder] || _('Search or filter results…')
|
||||
- block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
|
||||
|
||||
.issues-filters
|
||||
|
|
|
|||
|
|
@ -29,3 +29,5 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
.execute(merge_request)
|
||||
end
|
||||
end
|
||||
|
||||
MergeWorker.prepend_mod
|
||||
|
|
|
|||
|
|
@ -505,6 +505,15 @@ module Gitlab
|
|||
methods: %i(get head)
|
||||
end
|
||||
end
|
||||
|
||||
# Allow assets to be loaded to web-ide
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/421177
|
||||
allow do
|
||||
origins 'https://*.web-ide.gitlab-static.net'
|
||||
resource '/assets/webpack/*',
|
||||
credentials: false,
|
||||
methods: %i(get head)
|
||||
end
|
||||
end
|
||||
|
||||
# Use caching across all environments
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: abuse_report_labels
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128701/
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421373
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::anti-abuse
|
||||
default_enabled: false
|
||||
|
|
@ -465,7 +465,7 @@ if Gitlab.ee? && Settings['ee_cron_jobs']
|
|||
Settings.cron_jobs.merge!(Settings.ee_cron_jobs)
|
||||
end
|
||||
|
||||
Settings.cron_jobs['poll_interval'] ||= nil
|
||||
Settings.cron_jobs['poll_interval'] ||= ENV["GITLAB_CRON_JOBS_POLL_INTERVAL"] ? ENV["GITLAB_CRON_JOBS_POLL_INTERVAL"].to_i : nil
|
||||
Settings.cron_jobs['stuck_ci_jobs_worker'] ||= {}
|
||||
Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
|
||||
Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
|
||||
already have one.
|
||||
|
||||
You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#expiring-access-tokens) to expiring
|
||||
You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#access-token-expiration) to expiring
|
||||
tokens before GitLab 15.0 is released:
|
||||
|
||||
1. Edit the application.
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ feature_categories:
|
|||
description: User preferences for receiving notifications related to various actions within the application
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/31b0e53015e38e51d9c02cca85c9279600b1bf85
|
||||
milestone: '8.7'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrepareRemovalIndexDeploymentsOnIdWhereClusterIdPresent < Gitlab::Database::Migration[2.1]
|
||||
INDEX_NAME = 'index_deployments_on_id_where_cluster_id_present'
|
||||
|
||||
# TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/402510
|
||||
def up
|
||||
prepare_async_index_removal :deployments, :id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index :deployments, :id, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
0dd37cf1da3ff0f56f24a41dd76ef7cb789e0833a6ea73b773f56a8a3793c465
|
||||
|
|
@ -24835,6 +24835,7 @@ Represents a vulnerability.
|
|||
| <a id="vulnerabilityupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of when the vulnerability was last updated. |
|
||||
| <a id="vulnerabilityusernotescount"></a>`userNotesCount` | [`Int!`](#int) | Number of user notes attached to the vulnerability. |
|
||||
| <a id="vulnerabilityuserpermissions"></a>`userPermissions` | [`VulnerabilityPermissions!`](#vulnerabilitypermissions) | Permissions for the current user on the resource. |
|
||||
| <a id="vulnerabilityuuid"></a>`uuid` | [`String!`](#string) | UUID of the vulnerability finding. Can be used to look up the associated security report finding. |
|
||||
| <a id="vulnerabilityvulnerabilitypath"></a>`vulnerabilityPath` | [`String`](#string) | Path to the vulnerability's details page. |
|
||||
| <a id="vulnerabilityweburl"></a>`webUrl` | [`String`](#string) | URL to the vulnerability's details page. |
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ GET /groups/:id/-/debian_distributions/:codename
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The `codename` of a distribution. |
|
||||
| `codename` | string | yes | The `codename` of a distribution. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
|
||||
|
|
@ -122,7 +122,7 @@ GET /groups/:id/-/debian_distributions/:codename/key.asc
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The `codename` of a distribution. |
|
||||
| `codename` | string | yes | The `codename` of a distribution. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable/key.asc"
|
||||
|
|
@ -166,8 +166,8 @@ POST /groups/:id/-/debian_distributions
|
|||
| `version` | string | no | The version of the new Debian distribution. |
|
||||
| `description` | string | no | The description of the new Debian distribution. |
|
||||
| `valid_time_duration_seconds` | integer | no | The valid time duration (in seconds) of the new Debian distribution. |
|
||||
| `components` | architectures | no | The new Debian distribution's list of components. |
|
||||
| `architectures` | architectures | no | The new Debian distribution's list of architectures. |
|
||||
| `components` | string array | no | The new Debian distribution's list of components. |
|
||||
| `architectures` | string array | no | The new Debian distribution's list of architectures. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions?codename=sid"
|
||||
|
|
@ -213,8 +213,8 @@ PUT /groups/:id/-/debian_distributions/:codename
|
|||
| `version` | string | no | The Debian distribution's new version. |
|
||||
| `description` | string | no | The Debian distribution's new description. |
|
||||
| `valid_time_duration_seconds` | integer | no | The Debian distribution's new valid time duration (in seconds). |
|
||||
| `components` | architectures | no | The Debian distribution's new list of components. |
|
||||
| `architectures` | architectures | no | The Debian distribution's new list of architectures. |
|
||||
| `components` | string array | no | The Debian distribution's new list of components. |
|
||||
| `architectures` | string array | no | The Debian distribution's new list of architectures. |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable?suite=new-suite&valid_time_duration_seconds=604800"
|
||||
|
|
@ -253,7 +253,7 @@ DELETE /groups/:id/-/debian_distributions/:codename
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The codename of the Debian distribution. |
|
||||
| `codename` | string | yes | The codename of the Debian distribution. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ GET /projects/:id/debian_distributions/:codename
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The `codename` of a distribution. |
|
||||
| `codename` | string | yes | The `codename` of a distribution. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
|
||||
|
|
@ -121,7 +121,7 @@ GET /projects/:id/debian_distributions/:codename/key.asc
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The `codename` of a distribution. |
|
||||
| `codename` | string | yes | The `codename` of a distribution. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable/key.asc"
|
||||
|
|
@ -165,8 +165,8 @@ POST /projects/:id/debian_distributions
|
|||
| `version` | string | no | The new Debian distribution's version. |
|
||||
| `description` | string | no | The new Debian distribution's description. |
|
||||
| `valid_time_duration_seconds` | integer | no | The new Debian distribution's valid time duration (in seconds). |
|
||||
| `components` | architectures | no | The new Debian distribution's list of components. |
|
||||
| `architectures` | architectures | no | The new Debian distribution's list of architectures. |
|
||||
| `components` | string array | no | The new Debian distribution's list of components. |
|
||||
| `architectures` | string array | no | The new Debian distribution's list of architectures. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions?codename=sid"
|
||||
|
|
@ -212,8 +212,8 @@ PUT /projects/:id/debian_distributions/:codename
|
|||
| `version` | string | no | The Debian distribution's new version. |
|
||||
| `description` | string | no | The Debian distribution's new description. |
|
||||
| `valid_time_duration_seconds` | integer | no | The Debian distribution's new valid time duration (in seconds). |
|
||||
| `components` | architectures | no | The Debian distribution's new list of components. |
|
||||
| `architectures` | architectures | no | The Debian distribution's new list of architectures. |
|
||||
| `components` | string array | no | The Debian distribution's new list of components. |
|
||||
| `architectures` | string array | no | The Debian distribution's new list of architectures. |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable?suite=new-suite&valid_time_duration_seconds=604800"
|
||||
|
|
@ -252,7 +252,7 @@ DELETE /projects/:id/debian_distributions/:codename
|
|||
| Attribute | Type | Required | Description |
|
||||
| ---------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `codename` | integer | yes | The Debian distribution's codename. |
|
||||
| `codename` | string | yes | The Debian distribution's codename. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ Today only Users, Projects, Namespaces and container images are considered routa
|
|||
Initially, Organization routes will be [unscoped](../../../development/routing.md).
|
||||
Organizations will follow the path `https://gitlab.com/-/organizations/org-name/` as one of the design goals is that the addition of Organizations should not change existing Group and Project paths.
|
||||
|
||||
### Impact of the Organization on Other Features
|
||||
## Impact of the Organization on Other Features
|
||||
|
||||
We want a minimal amount of infrequently written tables in the shared database.
|
||||
If we have high write volume or large amounts of data in the shared database then this can become a single bottleneck for scaling and we lose the horizontal scalability objective of Cells.
|
||||
|
|
@ -277,6 +277,18 @@ With isolation being one of the main requirements to make Cells work, this means
|
|||
One exception to this are Users, which are stored in the cluster-wide shared database.
|
||||
For a deeper exploration of the impact on select features, see the [list of features impacted by Cells](../cells/index.md#impacted-features).
|
||||
|
||||
### Alignment between Organization and Fulfillment
|
||||
|
||||
Fulfillment is supportive of an entity above top-level groups. Their perspective is outlined in issue [#1138](https://gitlab.com/gitlab-org/fulfillment-meta/-/issues/1138).
|
||||
|
||||
#### Goals of Fulfillment
|
||||
|
||||
- Fulfillment has a longstanding plan to move billing from the top-level Group to a level above. This would mean that a license applies to an Organization and all its top-level Groups.
|
||||
- Fulfillment uses Zuora for billing and would like to have a 1-to-1 relationship between an Organization and their Zuora entity called BillingAccount. They want to move away from tying a license to a single top-level Group.
|
||||
- If a customer needs multiple Organizations, they will need to have a separate BillingAccount per each.
|
||||
- Ideally, a self-managed instance has a single Organization by default, which should be enough for most customers.
|
||||
- Fulfillment prefers only one additional entity.
|
||||
|
||||
## Iteration Plan
|
||||
|
||||
The following iteration plan outlines how we intend to arrive at the Organization MVC. We are following the guidelines for [Experiment, Beta, and Generally Available features](../../../policy/experiment-beta-support.md).
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ This guide explains:
|
|||
There are two types of [Debian packages](https://www.debian.org/doc/manuals/debian-faq/pkg-basics.en.html): binary and source.
|
||||
|
||||
- **Binary** - These are usually `.deb` files and contain executables, config files, and other data. A binary package must match your OS or architecture since it is already compiled. These are usually installed using `dpkg`. Dependencies must already exist on the system when installing a binary package.
|
||||
- **Source** - These are usual made up of `.dsc` files and `.gz` files. A source package is compiled on your system. These are fetched and installed with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html), which then uses `dpkg` after the package is compiled. When you use `apt`, it will fetch and install the necessary dependencies.
|
||||
- **Source** - These are usually made up of `.dsc` files and compressed `.tar` files. A source package may be compiled on your system.
|
||||
|
||||
The `.deb` file follows the naming convention `<PackageName>_<VersionNumber>-<DebianRevisionNumber>_<DebianArchitecture>.deb`
|
||||
Packages are fetched with [`apt`](https://manpages.debian.org/bullseye/apt/apt.8.en.html) and installed with `dpkg`. When you use `apt`, it also fetches and installs any dependencies.
|
||||
|
||||
It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info <deb_file>`
|
||||
The `.deb` file follows the naming convention `<PackageName>_<VersionNumber>-<DebianRevisionNumber>_<DebianArchitecture>.deb`.
|
||||
|
||||
It includes a `control file` that contains metadata about the package. You can view the control file by using `dpkg --info <deb_file>`.
|
||||
|
||||
The [`.changes` file](https://www.debian.org/doc/debian-policy/ch-controlfields.html#debian-changes-files-changes) is used to tell the Debian repository how to process updates to packages. It contains a variety of metadata for the package, including architecture, distribution, and version. In addition to the metadata, they contain three lists of checksums: `sha1`, `sha256`, and `md5` in the `Files` section. Refer to [sample_1.2.3~alpha2_amd64.changes](https://gitlab.com/gitlab-org/gitlab/-/blob/dd1e70d3676891025534dc4a1e89ca9383178fe7/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes) for an example of how these files are structured.
|
||||
|
||||
|
|
@ -40,8 +42,8 @@ When it comes to Debian, packages don't exist on their own. They belong to a _di
|
|||
## What does a Debian Repository look like?
|
||||
|
||||
- A [Debian repository](https://wiki.debian.org/DebianRepository) is made up of many releases.
|
||||
- Each release is given a **codename**. For the public Debian repository, these are things like "bullseye" and "jesse".
|
||||
- There is also the concept of **suites** which are essentially aliases of codenames synonymous with release channels like "stable" and "edge".
|
||||
- Each release is given a stable **codename**. For the public Debian repository, these are names like "bullseye" and "jessie".
|
||||
- There is also the concept of **suites** which are essentially aliases of codenames synonymous with release channels like "stable" and "edge". Over time they change and point to different _codenames_.
|
||||
- Each release has many **components**. In the public repository, these are "main", "contrib", and "non-free".
|
||||
- Each release has many **architectures** such as "amd64", "arm64", or "i386".
|
||||
- Each release has a signed **Release** file (see below about [GPG signing](#what-are-gpg-keys-and-what-are-signed-releases))
|
||||
|
|
|
|||
|
|
@ -7,15 +7,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Enforce two-factor authentication **(FREE ALL)**
|
||||
|
||||
Two-factor authentication (2FA) provides an additional level of security to your
|
||||
users' GitLab account. When enabled, users are prompted for a code generated by an application in
|
||||
addition to supplying their username and password to sign in.
|
||||
[Two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md)
|
||||
provides an additional level of security to your users' GitLab account. When enabled,
|
||||
users are prompted for a code generated by an application in addition to supplying
|
||||
their username and password to sign in.
|
||||
|
||||
NOTE:
|
||||
If you are [using and enforcing SSO](../user/group/saml_sso/index.md#sso-enforcement), you might already be enforcing 2FA on the identity provider (IDP) side. Enforcing 2FA on GitLab as well might be unnecessary.
|
||||
|
||||
Read more about [two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md).
|
||||
|
||||
## Enforce 2FA for all users **(FREE SELF)**
|
||||
|
||||
Users on GitLab can enable it without any administrator's intervention. If you
|
||||
|
|
@ -121,16 +120,19 @@ The target user is notified that 2FA has been disabled.
|
|||
|
||||
### For all users
|
||||
|
||||
There may be some special situations where you want to disable 2FA for everyone
|
||||
even when forced 2FA is disabled. There is a Rake task for that:
|
||||
To disable 2FA for all users even when forced 2FA is disabled, use the following Rake task.
|
||||
|
||||
```shell
|
||||
# Omnibus installations
|
||||
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
|
||||
- For installations that use the Linux package:
|
||||
|
||||
# Installations from source
|
||||
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
|
||||
```
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
|
||||
```
|
||||
|
||||
- For self-compiled installations:
|
||||
|
||||
```shell
|
||||
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
|
||||
```
|
||||
|
||||
## 2FA for Git over SSH operations **(PREMIUM ALL)**
|
||||
|
||||
|
|
|
|||
|
|
@ -3423,7 +3423,7 @@ By default, all new applications expire access tokens after 2 hours. In GitLab 1
|
|||
had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
|
||||
already have one.
|
||||
|
||||
You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#expiring-access-tokens) to expiring
|
||||
You should [opt in](https://docs.gitlab.com/ee/integration/oauth_provider.html#access-token-expiration) to expiring
|
||||
tokens before GitLab 15.0 is released:
|
||||
|
||||
1. Edit the application.
|
||||
|
|
|
|||
|
|
@ -114,22 +114,26 @@ To connect to multiple clusters, you must configure, register, and install an ag
|
|||
|
||||
#### Install the agent with Helm
|
||||
|
||||
WARNING:
|
||||
For simplicity, the default Helm chart configuration sets up a service account for the agent with `cluster-admin` rights. You should not use this on production systems. To deploy to a production system, follow the instructions in [Customize the Helm installation](#customize-the-helm-installation) to create a service account with the minimum permissions required for your deployment and specify that during installation.
|
||||
|
||||
To install the agent on your cluster using Helm:
|
||||
|
||||
1. [Install Helm](https://helm.sh/docs/intro/install/).
|
||||
1. In your computer, open a terminal and [connect to your cluster](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
|
||||
1. Run the command you copied when you [registered your agent with GitLab](#register-the-agent-with-gitlab).
|
||||
|
||||
Optionally, you can [customize the Helm installation](#customize-the-helm-installation). If you install the agent on a production system, you should customize the Helm installation to skip creating the service account.
|
||||
1. Optional. [Customize the Helm installation](#customize-the-helm-installation).
|
||||
If you install the agent on a production system, you should customize the Helm installation to restrict the permissions of the service account. See [How to deploy the GitLab Agent for Kubernetes with limited permissions](https://about.gitlab.com/blog/2021/09/10/setting-up-the-k-agent/).
|
||||
|
||||
##### Customize the Helm installation
|
||||
|
||||
By default, the Helm installation command generated by GitLab:
|
||||
|
||||
- Creates a namespace `gitlab-agent` for the deployment (`--namespace gitlab-agent`). You can skip creating the namespace by omitting the `--create-namespace` flag.
|
||||
- Sets up a service account for the agent with `cluster-admin` rights. You can:
|
||||
- Sets up a service account for the agent and assigns it the `cluster-admin` role. You can:
|
||||
- Skip creating the service account by adding `--set serviceAccount.create=false` to the `helm install` command. In this case, you must set `serviceAccount.name` to a pre-existing service account.
|
||||
- Skip creating the RBAC permissions by adding `--set rbac.create=false` to the `helm install` command. In this case, you must bring your own RBAC permissions for the agent. Otherwise, it has no permissions at all.
|
||||
- Customise the role assigned to the service account by adding `--set rbac.useExistingRole <your role name>` to the `helm install` command. In this case, you should have a pre-created role with restricted permissions that can be used by the service account.
|
||||
- Skip role assignment altogether by adding `--set rbac.create=false` to your `helm install` command. In this case, you must create `ClusterRoleBinding` manually.
|
||||
- Creates a `Secret` resource for the agent's access token. To instead bring your own secret with a token, omit the token (`--set token=...`) and instead use `--set config.secretName=<your secret name>`.
|
||||
- Creates a `Deployment` resource for the `agentk` pod.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ GitLab.com has the:
|
|||
- [`email_confirmation_setting`](../../administration/settings/sign_up_restrictions.md#confirm-user-email)
|
||||
setting set to **Hard**.
|
||||
- [`unconfirmed_users_delete_after_days`](../../administration/moderate_users.md#automatically-delete-unconfirmed-users)
|
||||
setting set to one day.
|
||||
setting set to three days.
|
||||
|
||||
## Password requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,18 @@ This section contains possible solutions for problems you might encounter.
|
|||
When you remove a user, they are removed from the group but their account is not deleted
|
||||
(see [remove access](scim_setup.md#remove-access)).
|
||||
|
||||
When the user is added back to the SCIM app, GitLab cannot create a new user because the user already exists.
|
||||
When the user is added back to the SCIM app, GitLab does not create a new user because the user already exists.
|
||||
|
||||
From August 11, 2023, the `skip_saml_identity_destroy_during_scim_deprovision` feature flag is enabled.
|
||||
|
||||
For a user de-provisioned by SCIM from that date, their SAML identity is not removed.
|
||||
|
||||
When that user is added back to the SCIM app:
|
||||
|
||||
- Their SCIM identity `active` attribute is set to `true`.
|
||||
- They can sign in using SSO.
|
||||
|
||||
For users de-provisioned by SCIM before that date, their SAML identity is destroyed.
|
||||
|
||||
To solve this problem:
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ Prerequisites:
|
|||
- The `dpkg-deb` binary must be installed on the GitLab instance.
|
||||
This binary is usually provided by the [`dpkg` package](https://wiki.debian.org/Teams/Dpkg/Downstream),
|
||||
installed by default on Debian and derivatives.
|
||||
- Support for compression algorithm ZStandard requires version `dpkg >=
|
||||
1.21.18` from Debian 12 Bookworm or `dpkg >= 1.19.0.5ubuntu2` from Ubuntu
|
||||
18.04 Bionic Beaver.
|
||||
|
||||
## Enable the Debian API **(FREE SELF)**
|
||||
|
||||
|
|
|
|||
|
|
@ -5017,6 +5017,9 @@ msgstr ""
|
|||
msgid "An error occurred while fetching label colors."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching labels, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while fetching participants"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5202,6 +5205,9 @@ msgstr[1] ""
|
|||
msgid "An error occurred while saving your settings. Try saving them again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while searching for labels, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while triggering the job."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26253,7 +26259,7 @@ msgstr ""
|
|||
msgid "Issues, merge requests, pushes, and comments."
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|After you begin creating issues for your projects, we can start tracking and displaying metrics for them"
|
||||
msgid "IssuesAnalytics|After you begin creating issues for your projects, we can start tracking and displaying metrics for them."
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|Avg/Month:"
|
||||
|
|
@ -26265,7 +26271,7 @@ msgstr ""
|
|||
msgid "IssuesAnalytics|Issues created per month"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|Last 12 months"
|
||||
msgid "IssuesAnalytics|Last 12 months (%{chartDateRange})"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|Sorry, your filter produced no results"
|
||||
|
|
@ -26274,7 +26280,7 @@ msgstr ""
|
|||
msgid "IssuesAnalytics|There are no issues for the projects in your group"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|To widen your search, change or remove filters in the filter bar above"
|
||||
msgid "IssuesAnalytics|To widen your search, change or remove filters in the filter bar above."
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuesAnalytics|Total:"
|
||||
|
|
@ -41795,12 +41801,6 @@ msgstr ""
|
|||
msgid "Search or filter commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search or filter results"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search or filter results..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Search or filter results…"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
|
|||
create(:ci_runner, :group, groups: [group], description: 'runner-foo')
|
||||
end
|
||||
|
||||
let_it_be(:group_runner_job) { create(:ci_build, runner: group_runner) }
|
||||
let_it_be(:group_runner_job) { create(:ci_build, runner: group_runner, project: project) }
|
||||
|
||||
context 'when logged in as group maintainer' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
|
|||
wait_for_requests
|
||||
|
||||
project.reload
|
||||
expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_custom_address)
|
||||
expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_alias_address)
|
||||
|
||||
page.within '#js-service-desk' do
|
||||
fill_in('service-desk-project-suffix', with: 'foo')
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
|
||||
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
|
||||
import UserDetails from '~/admin/abuse_report/components/user_details.vue';
|
||||
import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
|
||||
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
|
||||
import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
|
||||
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
|
||||
|
|
@ -19,13 +20,15 @@ describe('AbuseReportApp', () => {
|
|||
const findSimilarReportedContent = () =>
|
||||
findSimilarOpenReports().at(0).findComponent(ReportedContent);
|
||||
const findHistoryItems = () => wrapper.findComponent(HistoryItems);
|
||||
const findReportDetails = () => wrapper.findComponent(ReportDetails);
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
const createComponent = (props = {}, provide = {}) => {
|
||||
wrapper = shallowMountExtended(AbuseReportApp, {
|
||||
propsData: {
|
||||
abuseReport: mockAbuseReport,
|
||||
...props,
|
||||
},
|
||||
provide,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -104,6 +107,24 @@ describe('AbuseReportApp', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('ReportDetails', () => {
|
||||
describe('when abuseReportLabels feature flag is enabled', () => {
|
||||
it('renders ReportDetails', () => {
|
||||
createComponent({}, { glFeatures: { abuseReportLabels: true } });
|
||||
|
||||
expect(findReportDetails().props('reportId')).toBe(mockAbuseReport.report.globalId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when abuseReportLabels feature flag is disabled', () => {
|
||||
it('does not render ReportDetails', () => {
|
||||
createComponent({}, { glFeatures: { abuseReportLabels: false } });
|
||||
|
||||
expect(findReportDetails().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders ReportedContent', () => {
|
||||
expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
|
||||
import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import labelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
|
||||
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
|
||||
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
|
||||
import { createAlert } from '~/alert';
|
||||
import { mockLabelsQueryResponse, mockLabel1, mockLabel2 } from '../mock_data';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Labels select component', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const selectedText = () => wrapper.find('[data-testid="selected-labels"]').text();
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findEditButton = () => wrapper.findComponent(GlButton);
|
||||
const findDropdown = () => wrapper.findComponent(DropdownWidget);
|
||||
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
|
||||
|
||||
const labelsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockLabelsQueryResponse);
|
||||
const labelsQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
|
||||
|
||||
const updatePath = '/admin/abuse_reports/1';
|
||||
|
||||
async function openLabelsDropdown() {
|
||||
findEditButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
}
|
||||
|
||||
const selectLabel = (label) => {
|
||||
findDropdown().vm.$emit('set-option', label);
|
||||
nextTick();
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, labelsQueryHandler = labelsQueryHandlerSuccess } = {}) => {
|
||||
fakeApollo = createMockApollo([[labelsQuery, labelsQueryHandler]]);
|
||||
wrapper = shallowMount(LabelsSelect, {
|
||||
apolloProvider: fakeApollo,
|
||||
propsData: {
|
||||
report: { labels: [] },
|
||||
canEdit: true,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
updatePath,
|
||||
},
|
||||
stubs: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
DropdownWidget: stubComponent(DropdownWidget, {
|
||||
methods: { showDropdown: jest.fn() },
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('initial load', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('displays loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('disables edit button', () => {
|
||||
expect(findEditButton().props('disabled')).toEqual(true);
|
||||
});
|
||||
|
||||
describe('after initial load', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ report: { labels: [mockLabel1] } });
|
||||
});
|
||||
|
||||
it('does not display loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('enables edit button', () => {
|
||||
expect(findEditButton().props('disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('renders fetched labels in DropdownValue', () => {
|
||||
expect(findDropdownValue().isVisible()).toBe(true);
|
||||
expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no selected labels', () => {
|
||||
it('displays "None"', () => {
|
||||
createComponent();
|
||||
|
||||
expect(selectedText()).toContain('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are selected labels', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { report: { labels: [mockLabel1, mockLabel2] } } });
|
||||
|
||||
mock.onPut(updatePath).reply(HTTP_STATUS_OK, {});
|
||||
jest.spyOn(axios, 'put');
|
||||
});
|
||||
|
||||
it('renders selected labels in DropdownValue', () => {
|
||||
expect(findDropdownValue().isVisible()).toBe(true);
|
||||
expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1, mockLabel2]);
|
||||
});
|
||||
|
||||
it('selected labels can be removed', async () => {
|
||||
findDropdownValue().vm.$emit('onLabelRemove', mockLabel1.id);
|
||||
await nextTick();
|
||||
|
||||
expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel2]);
|
||||
expect(axios.put).toHaveBeenCalledWith(updatePath, {
|
||||
label_ids: [mockLabel2.id],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not editing', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not trigger abuse report labels query', () => {
|
||||
expect(labelsQueryHandlerSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render the dropdown', () => {
|
||||
expect(findDropdown().isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editing', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await openLabelsDropdown();
|
||||
});
|
||||
|
||||
it('triggers abuse report labels query', () => {
|
||||
expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders dropdown with fetched labels', () => {
|
||||
expect(findDropdown().isVisible()).toBe(true);
|
||||
expect(findDropdown().props('options')).toEqual([mockLabel1, mockLabel2]);
|
||||
});
|
||||
|
||||
it('selects/deselects a label', async () => {
|
||||
await selectLabel(mockLabel1);
|
||||
|
||||
expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
|
||||
|
||||
await selectLabel(mockLabel1);
|
||||
|
||||
expect(selectedText()).toContain('None');
|
||||
});
|
||||
|
||||
it('triggers abuse report labels query when search term is set', async () => {
|
||||
findDropdown().vm.$emit('set-search', 'Dos');
|
||||
await waitForPromises();
|
||||
|
||||
expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(2);
|
||||
expect(labelsQueryHandlerSuccess).toHaveBeenCalledWith({ searchTerm: 'Dos' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('after edit', () => {
|
||||
const setup = async (response) => {
|
||||
mock.onPut(updatePath).reply(response, {});
|
||||
jest.spyOn(axios, 'put');
|
||||
|
||||
createComponent();
|
||||
await openLabelsDropdown();
|
||||
await selectLabel(mockLabel1);
|
||||
|
||||
findDropdown().vm.$emit('hide');
|
||||
};
|
||||
|
||||
describe('successful save', () => {
|
||||
it('saves', async () => {
|
||||
await setup(HTTP_STATUS_OK);
|
||||
|
||||
expect(axios.put).toHaveBeenCalledWith(updatePath, {
|
||||
label_ids: [mockLabel1.id],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessful save', () => {
|
||||
it('creates an alert', async () => {
|
||||
await setup(HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while updating labels.',
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed abuse report labels query', () => {
|
||||
it('creates an alert', async () => {
|
||||
createComponent({ labelsQueryHandler: labelsQueryHandlerFailure });
|
||||
await openLabelsDropdown();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while searching for labels, please try again.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
|
||||
import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import abuseReportQuery from '~/admin/abuse_report/components/graphql/abuse_report.query.graphql';
|
||||
import { createAlert } from '~/alert';
|
||||
import { mockAbuseReport, mockLabel1, mockReportQueryResponse } from '../mock_data';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Report Details', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
|
||||
|
||||
const abuseReportQueryHandlerSuccess = jest.fn().mockResolvedValue(mockReportQueryResponse);
|
||||
const abuseReportQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
|
||||
|
||||
const createComponent = ({ abuseReportQueryHandler = abuseReportQueryHandlerSuccess } = {}) => {
|
||||
fakeApollo = createMockApollo([[abuseReportQuery, abuseReportQueryHandler]]);
|
||||
wrapper = shallowMount(ReportDetails, {
|
||||
apolloProvider: fakeApollo,
|
||||
propsData: {
|
||||
reportId: mockAbuseReport.report.globalId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('successful abuse report query', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('triggers abuse report query', async () => {
|
||||
await waitForPromises();
|
||||
|
||||
expect(abuseReportQueryHandlerSuccess).toHaveBeenCalledWith({
|
||||
id: mockAbuseReport.report.globalId,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders LabelsSelect with the fetched report', async () => {
|
||||
expect(findLabelsSelect().props('report').labels).toEqual([]);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findLabelsSelect().props('report').labels).toEqual([mockLabel1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed abuse report query', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ abuseReportQueryHandler: abuseReportQueryHandlerFailure });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('creates an alert', () => {
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while fetching labels, please try again.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -52,6 +52,7 @@ export const mockAbuseReport = {
|
|||
],
|
||||
},
|
||||
report: {
|
||||
globalId: 'gid://gitlab/AbuseReport/1',
|
||||
status: 'open',
|
||||
message: 'This is obvious spam',
|
||||
reportedAt: '2023-03-29T09:39:50.502Z',
|
||||
|
|
@ -73,3 +74,40 @@ export const mockAbuseReport = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockLabel1 = {
|
||||
id: 'gid://gitlab/AbuseReportLabel/1',
|
||||
title: 'Uno',
|
||||
color: '#F0AD4E',
|
||||
textColor: '#FFFFFF',
|
||||
description: null,
|
||||
};
|
||||
|
||||
export const mockLabel2 = {
|
||||
id: 'gid://gitlab/AbuseReportLabel/2',
|
||||
title: 'Dos',
|
||||
color: '#F0AD4E',
|
||||
textColor: '#FFFFFF',
|
||||
description: null,
|
||||
};
|
||||
|
||||
export const mockLabelsQueryResponse = {
|
||||
data: {
|
||||
abuseReportLabels: {
|
||||
nodes: [mockLabel1, mockLabel2],
|
||||
__typename: 'LabelConnection',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockReportQueryResponse = {
|
||||
data: {
|
||||
abuseReport: {
|
||||
labels: {
|
||||
nodes: [mockLabel1],
|
||||
__typename: 'LabelConnection',
|
||||
},
|
||||
__typename: 'AbuseReport',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe('Filtered Search Manager', () => {
|
|||
let manager;
|
||||
let tokensContainer;
|
||||
const page = 'issues';
|
||||
const placeholder = 'Search or filter results...';
|
||||
const placeholder = 'Search or filter results…';
|
||||
|
||||
function dispatchBackspaceEvent(element, eventType) {
|
||||
const event = new Event(eventType);
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ describe('IssuesDashboardApp component', () => {
|
|||
issuablesLoading: false,
|
||||
namespace: 'dashboard',
|
||||
recentSearchesStorageKey: 'issues',
|
||||
searchInputPlaceholder: i18n.searchPlaceholder,
|
||||
showPaginationControls: true,
|
||||
sortOptions: getSortOptions({
|
||||
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
|
||||
|
|
|
|||
|
|
@ -237,7 +237,6 @@ describe('CE IssuesListApp component', () => {
|
|||
expect(findIssuableList().props()).toMatchObject({
|
||||
namespace: defaultProvide.fullPath,
|
||||
recentSearchesStorageKey: 'issues',
|
||||
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
|
||||
sortOptions: getSortOptions({
|
||||
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
|
||||
hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ describe('AlertManagementEmptyState', () => {
|
|||
|
||||
it('renders the search component for incidents', () => {
|
||||
const filteredSearchBar = findFilteredSearchBar();
|
||||
expect(filteredSearchBar.props('searchInputPlaceholder')).toBe('Search or filter results…');
|
||||
|
||||
expect(filteredSearchBar.props('tokens')).toEqual([
|
||||
{
|
||||
type: TOKEN_TYPE_AUTHOR,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GlBreadcrumb } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
|
||||
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
|
||||
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
|
||||
|
|
@ -14,6 +14,7 @@ describe('Experimental new namespace creation app', () => {
|
|||
|
||||
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
|
||||
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
|
||||
const findTopBar = () => wrapper.findByTestId('top-bar');
|
||||
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
|
||||
const findImage = () => wrapper.find('img');
|
||||
const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
|
||||
|
|
@ -30,7 +31,7 @@ describe('Experimental new namespace creation app', () => {
|
|||
};
|
||||
|
||||
const createComponent = ({ slots, propsData } = {}) => {
|
||||
wrapper = shallowMount(NewNamespacePage, {
|
||||
wrapper = shallowMountExtended(NewNamespacePage, {
|
||||
slots,
|
||||
propsData: {
|
||||
...DEFAULT_PROPS,
|
||||
|
|
@ -167,4 +168,19 @@ describe('Experimental new namespace creation app', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('top bar', () => {
|
||||
it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => {
|
||||
gon.use_new_navigation = true;
|
||||
createComponent();
|
||||
|
||||
expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']);
|
||||
});
|
||||
|
||||
it('does not add classes when new navigation is not enabled', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findTopBar().classes()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ describe('WorkItemsListApp component', () => {
|
|||
issuablesLoading: true,
|
||||
namespace: 'work-items',
|
||||
recentSearchesStorageKey: 'issues',
|
||||
searchInputPlaceholder: 'Search or filter results...',
|
||||
searchTokens: [],
|
||||
showWorkItemTypeIcon: true,
|
||||
sortOptions: [],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::Ci::JobBaseField, feature_category: :runner_fleet do
|
||||
describe 'authorized?' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:object) { double }
|
||||
let(:ctx) { { current_user: current_user, current_field: current_field } }
|
||||
let(:current_field) { instance_double(described_class, original_name: current_field_name.to_sym) }
|
||||
let(:args) { {} }
|
||||
|
||||
subject(:field) do
|
||||
described_class.new(name: current_field_name, type: GraphQL::Types::String, null: true, **args)
|
||||
end
|
||||
|
||||
context 'when :job_field_authorization is specified' do
|
||||
let(:ctx) { { current_user: current_user, current_field: current_field, job_field_authorization: :foo } }
|
||||
|
||||
context 'with public field' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:current_field_name) do
|
||||
%i[allow_failure duration id kind status created_at finished_at queued_at queued_duration updated_at runner
|
||||
short_sha]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns true without authorizing' do
|
||||
is_expected.to be_authorized(object, nil, ctx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private field' do
|
||||
let(:current_field_name) { 'private_field' }
|
||||
|
||||
context 'when permission is not allowed' do
|
||||
it 'returns false' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
|
||||
|
||||
is_expected.not_to be_authorized(object, nil, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when permission is allowed' do
|
||||
it 'returns true' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
|
||||
|
||||
is_expected.to be_authorized(object, nil, ctx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :job_field_authorization is not specified' do
|
||||
let(:current_field_name) { 'status' }
|
||||
|
||||
it 'defaults to true' do
|
||||
is_expected.to be_authorized(object, nil, ctx)
|
||||
end
|
||||
|
||||
context 'when field is authorized' do
|
||||
let(:args) { { authorize: :foo } }
|
||||
|
||||
it 'tests the field authorization' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
|
||||
|
||||
expect(field).not_to be_authorized(object, nil, ctx)
|
||||
end
|
||||
|
||||
it 'tests the field authorization, if provided, when it succeeds' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
|
||||
|
||||
expect(field).to be_authorized(object, nil, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with field resolver' do
|
||||
let(:resolver) { Class.new }
|
||||
let(:args) { { resolver_class: resolver } }
|
||||
|
||||
it 'only tests the resolver authorization if it authorizes_object?' do
|
||||
is_expected.to be_authorized(object, nil, ctx)
|
||||
end
|
||||
|
||||
context 'when resolver authorizes object' do
|
||||
let(:resolver) do
|
||||
Class.new do
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
|
||||
authorizes_object!
|
||||
end
|
||||
end
|
||||
|
||||
it 'tests the resolver authorization, if provided' do
|
||||
expect(resolver).to receive(:authorized?).with(object, ctx).and_return(false)
|
||||
|
||||
expect(field).not_to be_authorized(object, nil, ctx)
|
||||
end
|
||||
|
||||
context 'when field is authorized' do
|
||||
let(:args) { { authorize: :foo, resolver_class: resolver } }
|
||||
|
||||
it 'tests field authorization before resolver authorization, when field auth fails' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(false)
|
||||
expect(resolver).not_to receive(:authorized?)
|
||||
|
||||
expect(field).not_to be_authorized(object, nil, ctx)
|
||||
end
|
||||
|
||||
it 'tests field authorization before resolver authorization, when field auth succeeds' do
|
||||
expect(Ability).to receive(:allowed?).with(current_user, :foo, object).and_return(true)
|
||||
expect(resolver).to receive(:authorized?).with(object, ctx).and_return(false)
|
||||
|
||||
expect(field).not_to be_authorized(object, nil, ctx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
context 'when late_extensions is given' do
|
||||
it 'registers the late extensions after the regular extensions' do
|
||||
extension_class = Class.new(GraphQL::Schema::Field::ConnectionExtension)
|
||||
field = described_class.new(name: 'private_field', type: GraphQL::Types::String.connection_type,
|
||||
null: true, late_extensions: [extension_class])
|
||||
|
||||
expect(field.extensions.last.class).to be(extension_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'Gitlab-style deprecations' do
|
||||
def subject(args = {})
|
||||
base_args = { name: 'private_field', type: GraphQL::Types::String, null: true }
|
||||
|
||||
described_class.new(**base_args.merge(args))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -688,6 +688,34 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
|
||||
it { is_expected.not_to include('logged-out-marketing-header') }
|
||||
|
||||
context 'when show_super_sidebar? is true' do
|
||||
context 'when @hide_top_bar_padding is false' do
|
||||
before do
|
||||
allow(helper).to receive(:show_super_sidebar?).and_return(true)
|
||||
helper.instance_variable_set(:@hide_top_bar_padding, false)
|
||||
end
|
||||
|
||||
it { is_expected.to include('with-top-bar') }
|
||||
end
|
||||
|
||||
context 'when @hide_top_bar_padding is true' do
|
||||
before do
|
||||
allow(helper).to receive(:show_super_sidebar?).and_return(true)
|
||||
helper.instance_variable_set(:@hide_top_bar_padding, true)
|
||||
end
|
||||
|
||||
it { is_expected.not_to include('with-top-bar') }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when show_super_sidebar? is false' do
|
||||
before do
|
||||
allow(helper).to receive(:show_super_sidebar?).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.not_to include('with-top-bar') }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dispensable_render' do
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ RSpec.describe ::API::Entities::Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.service_desk_address' do
|
||||
describe '.service_desk_address', feature_category: :service_desk do
|
||||
before do
|
||||
allow(project).to receive(:service_desk_enabled?).and_return(true)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ RSpec.describe 'cross-database foreign keys' do
|
|||
'merge_requests.updated_by_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
|
||||
'merge_requests.merge_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
|
||||
'merge_requests.author_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
|
||||
'notification_settings.user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421414
|
||||
'projects.creator_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844
|
||||
'projects.marked_for_deletion_by_user_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/421844
|
||||
'routes.namespace_id', # https://gitlab.com/gitlab-org/gitlab/-/issues/420869
|
||||
|
|
|
|||
|
|
@ -2380,7 +2380,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
end
|
||||
end
|
||||
|
||||
describe '#service_desk_address' do
|
||||
describe '#service_desk_address', feature_category: :service_desk do
|
||||
let_it_be(:project, reload: true) { create(:project, service_desk_enabled: true) }
|
||||
|
||||
subject { project.service_desk_address }
|
||||
|
|
@ -2424,7 +2424,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
end
|
||||
|
||||
context 'when project_key is set' do
|
||||
it 'returns custom address including the project_key' do
|
||||
it 'returns Service Desk alias address including the project_key' do
|
||||
create(:service_desk_setting, project: project, project_key: 'key1')
|
||||
|
||||
expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
|
||||
|
|
@ -2432,11 +2432,35 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
end
|
||||
|
||||
context 'when project_key is not set' do
|
||||
it 'returns custom address including the project full path' do
|
||||
it 'returns Service Desk alias address including the project full path' do
|
||||
expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom email is enabled' do
|
||||
let(:custom_email) { 'support@example.com' }
|
||||
|
||||
before do
|
||||
setting = ServiceDeskSetting.new(project: project, custom_email: custom_email, custom_email_enabled: true)
|
||||
allow(project).to receive(:service_desk_setting).and_return(setting)
|
||||
end
|
||||
|
||||
it 'returns custom email address' do
|
||||
expect(subject).to eq(custom_email)
|
||||
end
|
||||
|
||||
context 'when feature flag service_desk_custom_email is disabled' do
|
||||
before do
|
||||
stub_feature_flags(service_desk_custom_email: false)
|
||||
end
|
||||
|
||||
it 'returns custom email address' do
|
||||
# Don't check for a specific value. Just make sure it's not the custom email
|
||||
expect(subject).not_to eq(custom_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_service_desk_key' do
|
||||
|
|
|
|||
|
|
@ -53,13 +53,62 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'moderates user' do
|
||||
describe 'PUT #update' do
|
||||
let_it_be(:report) { create(:abuse_report) }
|
||||
let_it_be(:label1) { create(:abuse_report_label, title: 'Uno') }
|
||||
|
||||
let(:params) { { label_ids: [Gitlab::GlobalId.build(label1, id: label1.id).to_s] } }
|
||||
let(:expected_params) { ActionController::Parameters.new(params).permit! }
|
||||
|
||||
subject(:request) { put admin_abuse_report_path(report, params) }
|
||||
|
||||
it 'invokes the Admin::AbuseReports::UpdateService' do
|
||||
expect_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
|
||||
expect(service).to receive(:execute).and_call_original
|
||||
end
|
||||
|
||||
request
|
||||
end
|
||||
|
||||
context 'when the service response is a success' do
|
||||
before do
|
||||
allow_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
|
||||
allow(service).to receive(:execute).and_return(ServiceResponse.success)
|
||||
end
|
||||
|
||||
request
|
||||
end
|
||||
|
||||
it 'returns with a success status' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the service response is an error' do
|
||||
let(:error_message) { 'Error updating abuse report' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Admin::AbuseReports::UpdateService, report, admin, expected_params) do |service|
|
||||
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
|
||||
end
|
||||
|
||||
request
|
||||
end
|
||||
|
||||
it 'returns the service response message with a failed status' do
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(json_response['message']).to eq(error_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #moderate_user' do
|
||||
let(:report) { create(:abuse_report) }
|
||||
let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
|
||||
let(:expected_params) { ActionController::Parameters.new(params).permit! }
|
||||
let(:message) { 'Service response' }
|
||||
|
||||
subject(:request) { put path }
|
||||
subject(:request) { put moderate_user_admin_abuse_report_path(report, params) }
|
||||
|
||||
it 'invokes the Admin::AbuseReports::ModerateUserService' do
|
||||
expect_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
|
||||
|
|
@ -100,18 +149,6 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:path) { admin_abuse_report_path(report, params) }
|
||||
|
||||
it_behaves_like 'moderates user'
|
||||
end
|
||||
|
||||
describe 'PUT #moderate_user' do
|
||||
let(:path) { moderate_user_admin_abuse_report_path(report, params) }
|
||||
|
||||
it_behaves_like 'moderates user'
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let!(:report) { create(:abuse_report) }
|
||||
let(:params) { {} }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
||||
RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :runner_fleet do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user, :admin) }
|
||||
|
|
@ -228,7 +228,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with build running', :freeze_time do
|
||||
context 'with build running' do
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project1) }
|
||||
let!(:runner_manager) do
|
||||
create(:ci_runner_machine,
|
||||
|
|
@ -357,6 +357,77 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'jobs' do
|
||||
let(:query) do
|
||||
%(
|
||||
query {
|
||||
runner(id: "#{project_runner.to_global_id}") { #{runner_query_fragment} }
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
context 'with a job from a non-owned project' do
|
||||
let(:runner_query_fragment) do
|
||||
%(
|
||||
id
|
||||
jobs {
|
||||
nodes {
|
||||
id status shortSha finishedAt duration queuedDuration tags webPath
|
||||
project { id }
|
||||
runner { id }
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:owned_project_owner) { create(:user) }
|
||||
let_it_be(:owned_project) { create(:project) }
|
||||
let_it_be(:other_project) { create(:project) }
|
||||
let_it_be(:project_runner) { create(:ci_runner, :project_type, projects: [other_project, owned_project]) }
|
||||
let_it_be(:owned_project_pipeline) { create(:ci_pipeline, project: owned_project) }
|
||||
let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: other_project) }
|
||||
let_it_be(:owned_build) do
|
||||
create(:ci_build, :running, runner: project_runner, pipeline: owned_project_pipeline,
|
||||
tag_list: %i[a b c], created_at: 1.hour.ago, started_at: 59.minutes.ago, finished_at: 30.minutes.ago)
|
||||
end
|
||||
|
||||
let_it_be(:other_build) do
|
||||
create(:ci_build, :success, runner: project_runner, pipeline: other_project_pipeline,
|
||||
tag_list: %i[d e f], created_at: 30.minutes.ago, started_at: 19.minutes.ago, finished_at: 1.minute.ago)
|
||||
end
|
||||
|
||||
before_all do
|
||||
owned_project.add_owner(owned_project_owner)
|
||||
end
|
||||
|
||||
it 'returns empty values for sensitive fields in non-owned jobs' do
|
||||
post_graphql(query, current_user: owned_project_owner)
|
||||
|
||||
jobs_data = graphql_data_at(:runner, :jobs, :nodes)
|
||||
expect(jobs_data).not_to be_nil
|
||||
expect(jobs_data).to match([
|
||||
a_graphql_entity_for(other_build,
|
||||
status: other_build.status.upcase,
|
||||
project: nil, tags: nil, web_path: nil,
|
||||
runner: a_graphql_entity_for(project_runner),
|
||||
short_sha: other_build.short_sha, finished_at: other_build.finished_at&.iso8601,
|
||||
duration: a_value_within(0.001).of(other_build.duration),
|
||||
queued_duration: a_value_within(0.001).of((other_build.started_at - other_build.queued_at).to_f)),
|
||||
a_graphql_entity_for(owned_build,
|
||||
status: owned_build.status.upcase,
|
||||
project: a_graphql_entity_for(owned_project),
|
||||
tags: owned_build.tag_list.map(&:to_s),
|
||||
web_path: ::Gitlab::Routing.url_helpers.project_job_path(owned_project, owned_build),
|
||||
runner: a_graphql_entity_for(project_runner),
|
||||
short_sha: owned_build.short_sha,
|
||||
finished_at: owned_build.finished_at&.iso8601,
|
||||
duration: a_value_within(0.001).of(owned_build.duration),
|
||||
queued_duration: a_value_within(0.001).of((owned_build.started_at - owned_build.queued_at).to_f))
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for inactive runner' do
|
||||
|
|
@ -501,8 +572,14 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
end
|
||||
|
||||
describe 'for runner with status' do
|
||||
let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
|
||||
let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
|
||||
let_it_be(:stale_runner) do
|
||||
create(:ci_runner, description: 'Stale runner 1',
|
||||
created_at: (3.months + 1.second).ago, contacted_at: (3.months + 1.second).ago)
|
||||
end
|
||||
|
||||
let_it_be(:never_contacted_instance_runner) do
|
||||
create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil)
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
%(
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
|
|||
similar_open_report_hash = user_hash[:similar_open_reports][0]
|
||||
expect(similar_open_report_hash.keys).to match_array([
|
||||
:id,
|
||||
:global_id,
|
||||
:status,
|
||||
:message,
|
||||
:reported_at,
|
||||
|
|
@ -100,6 +101,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
|
|||
|
||||
expect(report_hash.keys).to match_array([
|
||||
:id,
|
||||
:global_id,
|
||||
:status,
|
||||
:message,
|
||||
:reported_at,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
|
|||
it 'exposes correct attributes' do
|
||||
expect(entity_hash.keys).to match_array([
|
||||
:id,
|
||||
:global_id,
|
||||
:status,
|
||||
:message,
|
||||
:reported_at,
|
||||
|
|
@ -29,6 +30,12 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
|
|||
])
|
||||
end
|
||||
|
||||
it 'includes correct value for global_id' do
|
||||
allow(Gitlab::GlobalId).to receive(:build).with(report, { id: report.id }).and_return(:mock_global_id)
|
||||
|
||||
expect(entity_hash[:global_id]).to eq 'mock_global_id'
|
||||
end
|
||||
|
||||
it 'correctly exposes `reporter`' do
|
||||
reporter_hash = entity_hash[:reporter]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::AbuseReports::UpdateService, feature_category: :instance_resiliency do
|
||||
let_it_be(:current_user) { create(:admin) }
|
||||
let_it_be(:abuse_report) { create(:abuse_report) }
|
||||
let_it_be(:label) { create(:abuse_report_label) }
|
||||
|
||||
let(:params) { {} }
|
||||
let(:service) { described_class.new(abuse_report, current_user, params) }
|
||||
|
||||
describe '#execute', :enable_admin_mode do
|
||||
subject { service.execute }
|
||||
|
||||
shared_examples 'returns an error response' do |error|
|
||||
it 'returns an error response' do
|
||||
expect(subject).to be_error
|
||||
expect(subject.message).to eq error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
describe 'invalid user' do
|
||||
describe 'when no user is given' do
|
||||
let_it_be(:current_user) { nil }
|
||||
|
||||
it_behaves_like 'returns an error response', 'Admin is required'
|
||||
end
|
||||
|
||||
describe 'when given user is not an admin' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
it_behaves_like 'returns an error response', 'Admin is required'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'invalid label_ids' do
|
||||
let(:params) { { label_ids: ['invalid_global_id', non_existing_record_id] } }
|
||||
|
||||
it 'does not update the abuse report' do
|
||||
expect { subject }.not_to change { abuse_report.labels }
|
||||
end
|
||||
|
||||
it { is_expected.to be_success }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with valid parameters' do
|
||||
context 'when label_ids is empty' do
|
||||
let(:params) { { label_ids: [] } }
|
||||
|
||||
context 'when abuse report has existing labels' do
|
||||
before do
|
||||
abuse_report.labels = [label]
|
||||
end
|
||||
|
||||
it 'clears the abuse report labels' do
|
||||
expect { subject }.to change { abuse_report.labels.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it { is_expected.to be_success }
|
||||
end
|
||||
|
||||
context 'when abuse report has no existing labels' do
|
||||
it 'does not update the abuse report' do
|
||||
expect { subject }.not_to change { abuse_report.labels }
|
||||
end
|
||||
|
||||
it { is_expected.to be_success }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when label_ids is not empty' do
|
||||
let(:params) { { label_ids: [Gitlab::GlobalId.build(label, id: label.id).to_s] } }
|
||||
|
||||
it 'updates the abuse report' do
|
||||
expect { subject }.to change { abuse_report.label_ids }.from([]).to([label.id])
|
||||
end
|
||||
|
||||
it { is_expected.to be_success }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -155,7 +155,7 @@ module FilteredSearchHelpers
|
|||
end
|
||||
|
||||
def default_placeholder
|
||||
'Search or filter results...'
|
||||
'Search or filter results…'
|
||||
end
|
||||
|
||||
def get_filtered_search_placeholder
|
||||
|
|
|
|||
Loading…
Reference in New Issue