Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									9919ffe0c8
								
							
						
					
					
						commit
						6a6e9bec88
					
				| 
						 | 
					@ -1,7 +1,9 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { GlAlert } from '@gitlab/ui';
 | 
					import { GlAlert } from '@gitlab/ui';
 | 
				
			||||||
 | 
					import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
				
			||||||
import ReportHeader from './report_header.vue';
 | 
					import ReportHeader from './report_header.vue';
 | 
				
			||||||
import UserDetails from './user_details.vue';
 | 
					import UserDetails from './user_details.vue';
 | 
				
			||||||
 | 
					import ReportDetails from './report_details.vue';
 | 
				
			||||||
import ReportedContent from './reported_content.vue';
 | 
					import ReportedContent from './reported_content.vue';
 | 
				
			||||||
import HistoryItems from './history_items.vue';
 | 
					import HistoryItems from './history_items.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,9 +19,11 @@ export default {
 | 
				
			||||||
    GlAlert,
 | 
					    GlAlert,
 | 
				
			||||||
    ReportHeader,
 | 
					    ReportHeader,
 | 
				
			||||||
    UserDetails,
 | 
					    UserDetails,
 | 
				
			||||||
 | 
					    ReportDetails,
 | 
				
			||||||
    ReportedContent,
 | 
					    ReportedContent,
 | 
				
			||||||
    HistoryItems,
 | 
					    HistoryItems,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  mixins: [glFeatureFlagsMixin()],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    abuseReport: {
 | 
					    abuseReport: {
 | 
				
			||||||
      type: Object,
 | 
					      type: Object,
 | 
				
			||||||
| 
						 | 
					@ -63,6 +67,12 @@ export default {
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <user-details v-if="abuseReport.user" :user="abuseReport.user" />
 | 
					    <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" />
 | 
					    <reported-content :report="abuseReport.report" data-testid="reported-content" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div v-for="report in similarOpenReports" :key="report.id" data-testid="similar-open-reports">
 | 
					    <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 Vue from 'vue';
 | 
				
			||||||
 | 
					import VueApollo from 'vue-apollo';
 | 
				
			||||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 | 
					import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 | 
				
			||||||
 | 
					import { defaultClient } from '~/graphql_shared/issuable_client';
 | 
				
			||||||
import AbuseReportApp from './components/abuse_report_app.vue';
 | 
					import AbuseReportApp from './components/abuse_report_app.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.use(VueApollo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const apolloProvider = new VueApollo({
 | 
				
			||||||
 | 
					  defaultClient,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const initAbuseReportApp = () => {
 | 
					export const initAbuseReportApp = () => {
 | 
				
			||||||
  const el = document.querySelector('#js-abuse-reports-detail-view');
 | 
					  const el = document.querySelector('#js-abuse-reports-detail-view');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +24,12 @@ export const initAbuseReportApp = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return new Vue({
 | 
					  return new Vue({
 | 
				
			||||||
    el,
 | 
					    el,
 | 
				
			||||||
 | 
					    apolloProvider,
 | 
				
			||||||
    name: 'AbuseReportAppRoot',
 | 
					    name: 'AbuseReportAppRoot',
 | 
				
			||||||
 | 
					    provide: {
 | 
				
			||||||
 | 
					      allowScopedLabels: false,
 | 
				
			||||||
 | 
					      updatePath: abuseReport.report.updatePath,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    render: (createElement) =>
 | 
					    render: (createElement) =>
 | 
				
			||||||
      createElement(AbuseReportApp, {
 | 
					      createElement(AbuseReportApp, {
 | 
				
			||||||
        props: {
 | 
					        props: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,7 +92,6 @@ export default {
 | 
				
			||||||
    :initial-filter-value="initialFilterValue"
 | 
					    :initial-filter-value="initialFilterValue"
 | 
				
			||||||
    :tokens="validTokens"
 | 
					    :tokens="validTokens"
 | 
				
			||||||
    :initial-sort-by="initialSortBy"
 | 
					    :initial-sort-by="initialSortBy"
 | 
				
			||||||
    :search-input-placeholder="__('Search or filter results...')"
 | 
					 | 
				
			||||||
    :search-text-option-label="s__('Runners|Search description...')"
 | 
					    :search-text-option-label="s__('Runners|Search description...')"
 | 
				
			||||||
    terms-as-tokens
 | 
					    terms-as-tokens
 | 
				
			||||||
    data-testid="runners-filtered-search"
 | 
					    data-testid="runners-filtered-search"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +42,7 @@ export default class FilteredSearchManager {
 | 
				
			||||||
    useDefaultState = false,
 | 
					    useDefaultState = false,
 | 
				
			||||||
    filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
 | 
					    filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
 | 
				
			||||||
    stateFiltersSelector = '.issues-state-filters',
 | 
					    stateFiltersSelector = '.issues-state-filters',
 | 
				
			||||||
    placeholder = __('Search or filter results...'),
 | 
					    placeholder = __('Search or filter results…'),
 | 
				
			||||||
    anchor = null,
 | 
					    anchor = null,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    this.isGroup = isGroup;
 | 
					    this.isGroup = isGroup;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -495,7 +495,6 @@ export default {
 | 
				
			||||||
    :issuables-loading="isLoading"
 | 
					    :issuables-loading="isLoading"
 | 
				
			||||||
    namespace="dashboard"
 | 
					    namespace="dashboard"
 | 
				
			||||||
    recent-searches-storage-key="issues"
 | 
					    recent-searches-storage-key="issues"
 | 
				
			||||||
    :search-input-placeholder="$options.i18n.searchPlaceholder"
 | 
					 | 
				
			||||||
    :search-tokens="searchTokens"
 | 
					    :search-tokens="searchTokens"
 | 
				
			||||||
    :show-pagination-controls="showPaginationControls"
 | 
					    :show-pagination-controls="showPaginationControls"
 | 
				
			||||||
    show-work-item-type-icon
 | 
					    show-work-item-type-icon
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -966,7 +966,6 @@ export default {
 | 
				
			||||||
      v-if="hasAnyIssues"
 | 
					      v-if="hasAnyIssues"
 | 
				
			||||||
      :namespace="fullPath"
 | 
					      :namespace="fullPath"
 | 
				
			||||||
      recent-searches-storage-key="issues"
 | 
					      recent-searches-storage-key="issues"
 | 
				
			||||||
      :search-input-placeholder="$options.i18n.searchPlaceholder"
 | 
					 | 
				
			||||||
      :search-tokens="searchTokens"
 | 
					      :search-tokens="searchTokens"
 | 
				
			||||||
      :has-scoped-labels-feature="hasScopedLabelsFeature"
 | 
					      :has-scoped-labels-feature="hasScopedLabelsFeature"
 | 
				
			||||||
      :initial-filter-value="filterTokens"
 | 
					      :initial-filter-value="filterTokens"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -121,7 +121,6 @@ export const i18n = {
 | 
				
			||||||
  reorderError: __('An error occurred while reordering issues.'),
 | 
					  reorderError: __('An error occurred while reordering issues.'),
 | 
				
			||||||
  deleteError: __('An error occurred while deleting an issuable.'),
 | 
					  deleteError: __('An error occurred while deleting an issuable.'),
 | 
				
			||||||
  rssLabel: __('Subscribe to RSS feed'),
 | 
					  rssLabel: __('Subscribe to RSS feed'),
 | 
				
			||||||
  searchPlaceholder: __('Search or filter results...'),
 | 
					 | 
				
			||||||
  upvotes: __('Upvotes'),
 | 
					  upvotes: __('Upvotes'),
 | 
				
			||||||
  titles: __('Titles'),
 | 
					  titles: __('Titles'),
 | 
				
			||||||
  descriptions: __('Descriptions'),
 | 
					  descriptions: __('Descriptions'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,6 @@ import reorderServiceDeskIssuesMutation from '../queries/reorder_service_desk_is
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  errorFetchingCounts,
 | 
					  errorFetchingCounts,
 | 
				
			||||||
  errorFetchingIssues,
 | 
					  errorFetchingIssues,
 | 
				
			||||||
  searchPlaceholder,
 | 
					 | 
				
			||||||
  issueRepositioningMessage,
 | 
					  issueRepositioningMessage,
 | 
				
			||||||
  reorderError,
 | 
					  reorderError,
 | 
				
			||||||
  SERVICE_DESK_BOT_USERNAME,
 | 
					  SERVICE_DESK_BOT_USERNAME,
 | 
				
			||||||
| 
						 | 
					@ -77,7 +76,6 @@ export default {
 | 
				
			||||||
  i18n: {
 | 
					  i18n: {
 | 
				
			||||||
    errorFetchingCounts,
 | 
					    errorFetchingCounts,
 | 
				
			||||||
    errorFetchingIssues,
 | 
					    errorFetchingIssues,
 | 
				
			||||||
    searchPlaceholder,
 | 
					 | 
				
			||||||
    issueRepositioningMessage,
 | 
					    issueRepositioningMessage,
 | 
				
			||||||
    reorderError,
 | 
					    reorderError,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -559,7 +557,6 @@ export default {
 | 
				
			||||||
      namespace="service-desk"
 | 
					      namespace="service-desk"
 | 
				
			||||||
      recent-searches-storage-key="service-desk-issues"
 | 
					      recent-searches-storage-key="service-desk-issues"
 | 
				
			||||||
      :error="issuesError"
 | 
					      :error="issuesError"
 | 
				
			||||||
      :search-input-placeholder="$options.i18n.searchPlaceholder"
 | 
					 | 
				
			||||||
      :search-tokens="searchTokens"
 | 
					      :search-tokens="searchTokens"
 | 
				
			||||||
      :issuables-loading="isLoading"
 | 
					      :issuables-loading="isLoading"
 | 
				
			||||||
      :initial-filter-value="filterTokens"
 | 
					      :initial-filter-value="filterTokens"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -235,7 +235,6 @@ export const noSearchResultsDescription = __(
 | 
				
			||||||
  'To widen your search, change or remove filters above',
 | 
					  'To widen your search, change or remove filters above',
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
export const noSearchResultsTitle = __('Sorry, your filter produced no results');
 | 
					export const noSearchResultsTitle = __('Sorry, your filter produced no results');
 | 
				
			||||||
export const searchPlaceholder = __('Search or filter results...');
 | 
					 | 
				
			||||||
export const issueRepositioningMessage = __(
 | 
					export const issueRepositioningMessage = __(
 | 
				
			||||||
  'Issues are being rebalanced at the moment, so manual reordering is disabled.',
 | 
					  'Issues are being rebalanced at the moment, so manual reordering is disabled.',
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import $ from 'jquery';
 | 
					import $ from 'jquery';
 | 
				
			||||||
 | 
					import { InternalEvents } from '~/tracking';
 | 
				
			||||||
import { __ } from './locale';
 | 
					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() {
 | 
					export default function initSettingsPanels() {
 | 
				
			||||||
  $('.settings').each((i, elm) => {
 | 
					  $('.settings').each((i, elm) => {
 | 
				
			||||||
    const $section = $(elm);
 | 
					    const $section = $(elm);
 | 
				
			||||||
| 
						 | 
					@ -64,4 +74,6 @@ export default function initSettingsPanels() {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initTrackProductAnalyticsExpanded();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,7 +73,8 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    searchInputPlaceholder: {
 | 
					    searchInputPlaceholder: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: __('Search or filter results…'),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    suggestionsListClass: {
 | 
					    suggestionsListClass: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,3 @@
 | 
				
			||||||
import { __ } from '~/locale';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const tdClass =
 | 
					export const tdClass =
 | 
				
			||||||
  'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
 | 
					  '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';
 | 
					export const thClass = 'gl-hover-bg-blue-50';
 | 
				
			||||||
| 
						 | 
					@ -15,7 +13,3 @@ export const initialPaginationState = {
 | 
				
			||||||
  firstPageSize: defaultPageSize,
 | 
					  firstPageSize: defaultPageSize,
 | 
				
			||||||
  lastPageSize: null,
 | 
					  lastPageSize: null,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const defaultI18n = {
 | 
					 | 
				
			||||||
  searchPlaceholder: __('Search or filter results…'),
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,11 +14,10 @@ import {
 | 
				
			||||||
} from '~/vue_shared/components/filtered_search_bar/constants';
 | 
					} from '~/vue_shared/components/filtered_search_bar/constants';
 | 
				
			||||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
 | 
					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 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';
 | 
					import { isAny } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  defaultI18n,
 | 
					 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    GlAlert,
 | 
					    GlAlert,
 | 
				
			||||||
    GlBadge,
 | 
					    GlBadge,
 | 
				
			||||||
| 
						 | 
					@ -300,7 +299,6 @@ export default {
 | 
				
			||||||
    <div class="filtered-search-wrapper">
 | 
					    <div class="filtered-search-wrapper">
 | 
				
			||||||
      <filtered-search-bar
 | 
					      <filtered-search-bar
 | 
				
			||||||
        :namespace="projectPath"
 | 
					        :namespace="projectPath"
 | 
				
			||||||
        :search-input-placeholder="$options.defaultI18n.searchPlaceholder"
 | 
					 | 
				
			||||||
        :tokens="filteredSearchTokens"
 | 
					        :tokens="filteredSearchTokens"
 | 
				
			||||||
        :initial-filter-value="filteredSearchValue"
 | 
					        :initial-filter-value="filteredSearchValue"
 | 
				
			||||||
        initial-sortby="created_desc"
 | 
					        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 PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
 | 
				
			||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 | 
					import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 | 
				
			||||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
 | 
					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 FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
 | 
				
			||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
					import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +52,8 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    searchInputPlaceholder: {
 | 
					    searchInputPlaceholder: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: false,
 | 
				
			||||||
 | 
					      default: __('Search or filter results…'),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    searchTokens: {
 | 
					    searchTokens: {
 | 
				
			||||||
      type: Array,
 | 
					      type: Array,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,6 +88,10 @@ export default {
 | 
				
			||||||
    showSuperSidebarToggle() {
 | 
					    showSuperSidebarToggle() {
 | 
				
			||||||
      return gon.use_new_navigation && sidebarState.isCollapsed;
 | 
					      return gon.use_new_navigation && sidebarState.isCollapsed;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    topBarClasses() {
 | 
				
			||||||
 | 
					      return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
| 
						 | 
					@ -120,15 +124,17 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <div
 | 
					    <div :class="topBarClasses" data-testid="top-bar">
 | 
				
			||||||
      class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
 | 
					      <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"
 | 
					        <super-sidebar-toggle
 | 
				
			||||||
        class="gl-mr-2"
 | 
					          v-if="showSuperSidebarToggle"
 | 
				
			||||||
        :class="$options.JS_TOGGLE_EXPAND_CLASS"
 | 
					          class="gl-mr-2"
 | 
				
			||||||
      />
 | 
					          :class="$options.JS_TOGGLE_EXPAND_CLASS"
 | 
				
			||||||
      <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
 | 
					        />
 | 
				
			||||||
 | 
					        <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <template v-if="activePanel">
 | 
					    <template v-if="activePanel">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,9 +10,6 @@ import { STATE_CLOSED } from '../../constants';
 | 
				
			||||||
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
 | 
					import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  i18n: {
 | 
					 | 
				
			||||||
    searchPlaceholder: __('Search or filter results...'),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  issuableListTabs,
 | 
					  issuableListTabs,
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    IssuableList,
 | 
					    IssuableList,
 | 
				
			||||||
| 
						 | 
					@ -64,7 +61,6 @@ export default {
 | 
				
			||||||
    :issuables-loading="$apollo.queries.workItems.loading"
 | 
					    :issuables-loading="$apollo.queries.workItems.loading"
 | 
				
			||||||
    namespace="work-items"
 | 
					    namespace="work-items"
 | 
				
			||||||
    recent-searches-storage-key="issues"
 | 
					    recent-searches-storage-key="issues"
 | 
				
			||||||
    :search-input-placeholder="$options.i18n.searchPlaceholder"
 | 
					 | 
				
			||||||
    :search-tokens="searchTokens"
 | 
					    :search-tokens="searchTokens"
 | 
				
			||||||
    show-work-item-type-icon
 | 
					    show-work-item-type-icon
 | 
				
			||||||
    :sort-options="sortOptions"
 | 
					    :sort-options="sortOptions"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,10 @@ class Admin::AbuseReportsController < Admin::ApplicationController
 | 
				
			||||||
  feature_category :insider_threat
 | 
					  feature_category :insider_threat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_action :set_status_param, only: :index, if: -> { Feature.enabled?(:abuse_reports_list) }
 | 
					  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
 | 
					  def index
 | 
				
			||||||
    @abuse_reports = AbuseReportsFinder.new(params).execute
 | 
					    @abuse_reports = AbuseReportsFinder.new(params).execute
 | 
				
			||||||
| 
						 | 
					@ -12,14 +15,11 @@ class Admin::AbuseReportsController < Admin::ApplicationController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show; end
 | 
					  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
 | 
					  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?
 | 
					    if response.success?
 | 
				
			||||||
      render json: { message: response.message }
 | 
					      head :ok
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render json: { message: response.message }, status: :unprocessable_entity
 | 
					      render json: { message: response.message }, status: :unprocessable_entity
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					@ -53,6 +53,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def permitted_params
 | 
					  def permitted_params
 | 
				
			||||||
    params.permit(:user_action, :close, :reason, :comment)
 | 
					    params.permit(:user_action, :close, :reason, :comment, { label_ids: [] })
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
 | 
				
			||||||
    service_desk_settings = project.service_desk_setting
 | 
					    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,
 | 
					      service_desk_enabled: project.service_desk_enabled,
 | 
				
			||||||
      issue_template_key: service_desk_settings&.issue_template_key,
 | 
					      issue_template_key: service_desk_settings&.issue_template_key,
 | 
				
			||||||
      template_file_missing: service_desk_settings&.issue_template_missing?,
 | 
					      template_file_missing: service_desk_settings&.issue_template_missing?,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,8 @@ module Resolvers
 | 
				
			||||||
      alias_method :runner, :object
 | 
					      alias_method :runner, :object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      def resolve_with_lookahead(statuses: nil)
 | 
					      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
 | 
					        jobs = ::Ci::JobsFinder.new(current_user: current_user, runner: runner, params: { scope: statuses }).execute
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        apply_lookahead(jobs)
 | 
					        apply_lookahead(jobs)
 | 
				
			||||||
| 
						 | 
					@ -30,7 +32,7 @@ module Resolvers
 | 
				
			||||||
          previous_stage_jobs_or_needs: [:needs, :pipeline],
 | 
					          previous_stage_jobs_or_needs: [:needs, :pipeline],
 | 
				
			||||||
          artifacts: [:job_artifacts],
 | 
					          artifacts: [:job_artifacts],
 | 
				
			||||||
          pipeline: [:user],
 | 
					          pipeline: [:user],
 | 
				
			||||||
          project: [{ project: [:route, { namespace: [:route] }] }],
 | 
					          project: [{ project: [:route, { namespace: [:route] }, :project_feature] }],
 | 
				
			||||||
          detailed_status: [
 | 
					          detailed_status: [
 | 
				
			||||||
            :metadata,
 | 
					            :metadata,
 | 
				
			||||||
            { pipeline: [:merge_request] },
 | 
					            { 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'
 | 
					      graphql_name 'CiJob'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      present_using ::Ci::BuildPresenter
 | 
					      present_using ::Ci::BuildPresenter
 | 
				
			||||||
 | 
					      field_class Types::Ci::JobBaseField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      connection_type_class Types::LimitedCountableConnectionType
 | 
					      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 << '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 << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
 | 
				
			||||||
    class_names << 'with-performance-bar' if performance_bar_enabled?
 | 
					    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 << system_message_class
 | 
				
			||||||
    class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
 | 
					    class_names << 'logged-out-marketing-header' if !current_user && ::Gitlab.com? && !show_super_sidebar?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -479,7 +479,7 @@ module SearchHelper
 | 
				
			||||||
    end.to_json
 | 
					    end.to_json
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def search_filter_input_options(type, placeholder = _('Search or filter results...'))
 | 
					  def search_filter_input_options(type, placeholder = _('Search or filter results…'))
 | 
				
			||||||
    opts =
 | 
					    opts =
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        id: "filtered-search-#{type}",
 | 
					        id: "filtered-search-#{type}",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2963,7 +2963,11 @@ class Project < ApplicationRecord
 | 
				
			||||||
  alias_method :service_desk_enabled?, :service_desk_enabled
 | 
					  alias_method :service_desk_enabled?, :service_desk_enabled
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def service_desk_address
 | 
					  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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def service_desk_incoming_address
 | 
					  def service_desk_incoming_address
 | 
				
			||||||
| 
						 | 
					@ -2975,7 +2979,7 @@ class Project < ApplicationRecord
 | 
				
			||||||
    config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
 | 
					    config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def service_desk_custom_address
 | 
					  def service_desk_alias_address
 | 
				
			||||||
    return unless Gitlab::Email::ServiceDeskEmail.enabled?
 | 
					    return unless Gitlab::Email::ServiceDeskEmail.enabled?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    key = service_desk_setting&.project_key || default_service_desk_suffix
 | 
					    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}")
 | 
					    Gitlab::Email::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
 | 
				
			||||||
  end
 | 
					  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
 | 
					  def default_service_desk_suffix
 | 
				
			||||||
    "#{id}-issue-"
 | 
					    "#{id}-issue-"
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,9 @@ module Admin
 | 
				
			||||||
    include RequestAwareEntity
 | 
					    include RequestAwareEntity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expose :id
 | 
					    expose :id
 | 
				
			||||||
 | 
					    expose :global_id do |report|
 | 
				
			||||||
 | 
					      Gitlab::GlobalId.build(report, id: report.id).to_s
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
    expose :status
 | 
					    expose :status
 | 
				
			||||||
    expose :message
 | 
					    expose :message
 | 
				
			||||||
    expose :created_at, as: :reported_at
 | 
					    expose :created_at, as: :reported_at
 | 
				
			||||||
| 
						 | 
					@ -24,9 +27,6 @@ module Admin
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    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|
 | 
					    expose :update_path do |report|
 | 
				
			||||||
      admin_abuse_report_path(report)
 | 
					      admin_abuse_report_path(report)
 | 
				
			||||||
    end
 | 
					    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)
 | 
					      def add_recipients(users, type, reason)
 | 
				
			||||||
        if users.is_a?(ActiveRecord::Relation)
 | 
					        if users.is_a?(ActiveRecord::Relation)
 | 
				
			||||||
          users = users.includes(:notification_settings)
 | 
					          users = users.includes(:notification_settings)
 | 
				
			||||||
 | 
					            .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/421821')
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        users = Array(users).compact
 | 
					        users = Array(users).compact
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
!!! 5
 | 
					!!! 5
 | 
				
			||||||
- add_page_specific_style 'page_bundles/terms'
 | 
					- add_page_specific_style 'page_bundles/terms'
 | 
				
			||||||
- @hide_top_bar = true
 | 
					- @hide_top_bar = true
 | 
				
			||||||
 | 
					- @hide_top_bar_padding = true
 | 
				
			||||||
- body_classes = [user_application_theme]
 | 
					- body_classes = [user_application_theme]
 | 
				
			||||||
%html{ lang: I18n.locale, class: page_class }
 | 
					%html{ lang: I18n.locale, class: page_class }
 | 
				
			||||||
  = render "layouts/head"
 | 
					  = render "layouts/head"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@
 | 
				
			||||||
        enabled: "#{@project.service_desk_enabled}",
 | 
					        enabled: "#{@project.service_desk_enabled}",
 | 
				
			||||||
        issue_tracker_enabled: "#{@project.project_feature.issues_enabled?}",
 | 
					        issue_tracker_enabled: "#{@project.project_feature.issues_enabled?}",
 | 
				
			||||||
        incoming_email: (@project.service_desk_incoming_address if @project.service_desk_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?}",
 | 
					        service_desk_email_enabled: "#{Gitlab::Email::ServiceDeskEmail.enabled?}",
 | 
				
			||||||
        selected_template: "#{@project.service_desk_setting&.issue_template_key}",
 | 
					        selected_template: "#{@project.service_desk_setting&.issue_template_key}",
 | 
				
			||||||
        selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
 | 
					        selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
- type = local_assigns.fetch(:type)
 | 
					- type = local_assigns.fetch(:type)
 | 
				
			||||||
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
 | 
					- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
 | 
				
			||||||
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
 | 
					- 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' : ''
 | 
					- block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.issues-filters
 | 
					.issues-filters
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,3 +29,5 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
 | 
				
			||||||
      .execute(merge_request)
 | 
					      .execute(merge_request)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MergeWorker.prepend_mod
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -505,6 +505,15 @@ module Gitlab
 | 
				
			||||||
          methods: %i(get head)
 | 
					          methods: %i(get head)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      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
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Use caching across all environments
 | 
					    # 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)
 | 
					  Settings.cron_jobs.merge!(Settings.ee_cron_jobs)
 | 
				
			||||||
end
 | 
					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'] ||= {}
 | 
				
			||||||
Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
 | 
					Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *'
 | 
				
			||||||
Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker'
 | 
					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
 | 
					    had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
 | 
				
			||||||
    already have one.
 | 
					    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:
 | 
					    tokens before GitLab 15.0 is released:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    1. Edit the application.
 | 
					    1. Edit the application.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,4 +7,4 @@ feature_categories:
 | 
				
			||||||
description: User preferences for receiving notifications related to various actions within the application
 | 
					description: User preferences for receiving notifications related to various actions within the application
 | 
				
			||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/31b0e53015e38e51d9c02cca85c9279600b1bf85
 | 
					introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/31b0e53015e38e51d9c02cca85c9279600b1bf85
 | 
				
			||||||
milestone: '8.7'
 | 
					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="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="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="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="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. |
 | 
					| <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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
 | 
					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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable/key.asc"
 | 
					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. |
 | 
					| `version`                     | string         | no       | The version of the new Debian distribution. |
 | 
				
			||||||
| `description`                 | string         | no       | The description 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. |
 | 
					| `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. |
 | 
					| `components`                  | string array   | no       | The new Debian distribution's list of components. |
 | 
				
			||||||
| `architectures`               | architectures  | no       | The new Debian distribution's list of architectures. |
 | 
					| `architectures`               | string array   | no       | The new Debian distribution's list of architectures. |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions?codename=sid"
 | 
					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. |
 | 
					| `version`                     | string         | no       | The Debian distribution's new version. |
 | 
				
			||||||
| `description`                 | string         | no       | The Debian distribution's new description. |
 | 
					| `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). |
 | 
					| `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. |
 | 
					| `components`                  | string array   | no       | The Debian distribution's new list of components. |
 | 
				
			||||||
| `architectures`               | architectures  | no       | The Debian distribution's new list of architectures. |
 | 
					| `architectures`               | string array   | no       | The Debian distribution's new list of architectures. |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```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"
 | 
					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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/-/debian_distributions/unstable"
 | 
					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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
 | 
					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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable/key.asc"
 | 
					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. |
 | 
					| `version`                     | string         | no       | The new Debian distribution's version. |
 | 
				
			||||||
| `description`                 | string         | no       | The new Debian distribution's description. |
 | 
					| `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). |
 | 
					| `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. |
 | 
					| `components`                  | string array   | no       | The new Debian distribution's list of components. |
 | 
				
			||||||
| `architectures`               | architectures  | no       | The new Debian distribution's list of architectures. |
 | 
					| `architectures`               | string array   | no       | The new Debian distribution's list of architectures. |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions?codename=sid"
 | 
					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. |
 | 
					| `version`                     | string         | no       | The Debian distribution's new version. |
 | 
				
			||||||
| `description`                 | string         | no       | The Debian distribution's new description. |
 | 
					| `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). |
 | 
					| `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. |
 | 
					| `components`                  | string array   | no       | The Debian distribution's new list of components. |
 | 
				
			||||||
| `architectures`               | architectures  | no       | The Debian distribution's new list of architectures. |
 | 
					| `architectures`               | string array   | no       | The Debian distribution's new list of architectures. |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```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"
 | 
					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 |
 | 
					| 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. |
 | 
					| `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
 | 
					```shell
 | 
				
			||||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/debian_distributions/unstable"
 | 
					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).
 | 
					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.
 | 
					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.
 | 
					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.
 | 
					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.
 | 
					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).
 | 
					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
 | 
					## 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).
 | 
					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.
 | 
					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.
 | 
					- **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.
 | 
					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?
 | 
					## What does a Debian Repository look like?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- A [Debian repository](https://wiki.debian.org/DebianRepository) is made up of many releases.
 | 
					- 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".
 | 
					- 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".
 | 
					  - 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 **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 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))
 | 
					- 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)**
 | 
					# Enforce two-factor authentication **(FREE ALL)**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Two-factor authentication (2FA) provides an additional level of security to your
 | 
					[Two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md)
 | 
				
			||||||
users' GitLab account. When enabled, users are prompted for a code generated by an application in
 | 
					provides an additional level of security to your users' GitLab account. When enabled,
 | 
				
			||||||
addition to supplying their username and password to sign in.
 | 
					users are prompted for a code generated by an application in addition to supplying
 | 
				
			||||||
 | 
					their username and password to sign in.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NOTE:
 | 
					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.
 | 
					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)**
 | 
					## Enforce 2FA for all users **(FREE SELF)**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Users on GitLab can enable it without any administrator's intervention. If you
 | 
					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
 | 
					### For all users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
There may be some special situations where you want to disable 2FA for everyone
 | 
					To disable 2FA for all users even when forced 2FA is disabled, use the following Rake task.
 | 
				
			||||||
even when forced 2FA is disabled. There is a Rake task for that:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					- For installations that use the Linux package:
 | 
				
			||||||
# Omnibus installations
 | 
					 | 
				
			||||||
sudo gitlab-rake gitlab:two_factor:disable_for_all_users
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Installations from source
 | 
					  ```shell
 | 
				
			||||||
sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
 | 
					  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)**
 | 
					## 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
 | 
					had no expiration. In GitLab 15.0, an expiry will be automatically generated for any existing token that does not
 | 
				
			||||||
already have one.
 | 
					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:
 | 
					tokens before GitLab 15.0 is released:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Edit the application.
 | 
					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
 | 
					#### 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:
 | 
					To install the agent on your cluster using Helm:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. [Install Helm](https://helm.sh/docs/intro/install/).
 | 
					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. 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).
 | 
					1. Run the command you copied when you [registered your agent with GitLab](#register-the-agent-with-gitlab).
 | 
				
			||||||
 | 
					1. Optional. [Customize the Helm installation](#customize-the-helm-installation).
 | 
				
			||||||
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.
 | 
					   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
 | 
					##### Customize the Helm installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
By default, the Helm installation command generated by GitLab:
 | 
					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.
 | 
					- 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 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 `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.
 | 
					- 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)
 | 
					- [`email_confirmation_setting`](../../administration/settings/sign_up_restrictions.md#confirm-user-email)
 | 
				
			||||||
  setting set to **Hard**.
 | 
					  setting set to **Hard**.
 | 
				
			||||||
- [`unconfirmed_users_delete_after_days`](../../administration/moderate_users.md#automatically-delete-unconfirmed-users)
 | 
					- [`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
 | 
					## 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
 | 
					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)).
 | 
					(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:
 | 
					To solve this problem:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,9 @@ Prerequisites:
 | 
				
			||||||
- The `dpkg-deb` binary must be installed on the GitLab instance.
 | 
					- 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),
 | 
					  This binary is usually provided by the [`dpkg` package](https://wiki.debian.org/Teams/Dpkg/Downstream),
 | 
				
			||||||
  installed by default on Debian and derivatives.
 | 
					  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)**
 | 
					## Enable the Debian API **(FREE SELF)**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5017,6 +5017,9 @@ msgstr ""
 | 
				
			||||||
msgid "An error occurred while fetching label colors."
 | 
					msgid "An error occurred while fetching label colors."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					msgid "An error occurred while fetching labels, please try again."
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "An error occurred while fetching participants"
 | 
					msgid "An error occurred while fetching participants"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5202,6 +5205,9 @@ msgstr[1] ""
 | 
				
			||||||
msgid "An error occurred while saving your settings. Try saving them again."
 | 
					msgid "An error occurred while saving your settings. Try saving them again."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					msgid "An error occurred while searching for labels, please try again."
 | 
				
			||||||
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "An error occurred while triggering the job."
 | 
					msgid "An error occurred while triggering the job."
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26253,7 +26259,7 @@ msgstr ""
 | 
				
			||||||
msgid "Issues, merge requests, pushes, and comments."
 | 
					msgid "Issues, merge requests, pushes, and comments."
 | 
				
			||||||
msgstr ""
 | 
					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 ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "IssuesAnalytics|Avg/Month:"
 | 
					msgid "IssuesAnalytics|Avg/Month:"
 | 
				
			||||||
| 
						 | 
					@ -26265,7 +26271,7 @@ msgstr ""
 | 
				
			||||||
msgid "IssuesAnalytics|Issues created per month"
 | 
					msgid "IssuesAnalytics|Issues created per month"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "IssuesAnalytics|Last 12 months"
 | 
					msgid "IssuesAnalytics|Last 12 months (%{chartDateRange})"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "IssuesAnalytics|Sorry, your filter produced no results"
 | 
					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"
 | 
					msgid "IssuesAnalytics|There are no issues for the projects in your group"
 | 
				
			||||||
msgstr ""
 | 
					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 ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "IssuesAnalytics|Total:"
 | 
					msgid "IssuesAnalytics|Total:"
 | 
				
			||||||
| 
						 | 
					@ -41795,12 +41801,6 @@ msgstr ""
 | 
				
			||||||
msgid "Search or filter commits"
 | 
					msgid "Search or filter commits"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Search or filter results"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
msgid "Search or filter results..."
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
msgid "Search or filter results…"
 | 
					msgid "Search or filter results…"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -249,7 +249,7 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
 | 
				
			||||||
      create(:ci_runner, :group, groups: [group], description: 'runner-foo')
 | 
					      create(:ci_runner, :group, groups: [group], description: 'runner-foo')
 | 
				
			||||||
    end
 | 
					    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
 | 
					    context 'when logged in as group maintainer' do
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache, feature_c
 | 
				
			||||||
      wait_for_requests
 | 
					      wait_for_requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      project.reload
 | 
					      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
 | 
					      page.within '#js-service-desk' do
 | 
				
			||||||
        fill_in('service-desk-project-suffix', with: 'foo')
 | 
					        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 AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
 | 
				
			||||||
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
 | 
					import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
 | 
				
			||||||
import UserDetails from '~/admin/abuse_report/components/user_details.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 ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
 | 
				
			||||||
import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
 | 
					import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
 | 
				
			||||||
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
 | 
					import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
 | 
				
			||||||
| 
						 | 
					@ -19,13 +20,15 @@ describe('AbuseReportApp', () => {
 | 
				
			||||||
  const findSimilarReportedContent = () =>
 | 
					  const findSimilarReportedContent = () =>
 | 
				
			||||||
    findSimilarOpenReports().at(0).findComponent(ReportedContent);
 | 
					    findSimilarOpenReports().at(0).findComponent(ReportedContent);
 | 
				
			||||||
  const findHistoryItems = () => wrapper.findComponent(HistoryItems);
 | 
					  const findHistoryItems = () => wrapper.findComponent(HistoryItems);
 | 
				
			||||||
 | 
					  const findReportDetails = () => wrapper.findComponent(ReportDetails);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = (props = {}) => {
 | 
					  const createComponent = (props = {}, provide = {}) => {
 | 
				
			||||||
    wrapper = shallowMountExtended(AbuseReportApp, {
 | 
					    wrapper = shallowMountExtended(AbuseReportApp, {
 | 
				
			||||||
      propsData: {
 | 
					      propsData: {
 | 
				
			||||||
        abuseReport: mockAbuseReport,
 | 
					        abuseReport: mockAbuseReport,
 | 
				
			||||||
        ...props,
 | 
					        ...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', () => {
 | 
					  it('renders ReportedContent', () => {
 | 
				
			||||||
    expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
 | 
					    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: {
 | 
					  report: {
 | 
				
			||||||
 | 
					    globalId: 'gid://gitlab/AbuseReport/1',
 | 
				
			||||||
    status: 'open',
 | 
					    status: 'open',
 | 
				
			||||||
    message: 'This is obvious spam',
 | 
					    message: 'This is obvious spam',
 | 
				
			||||||
    reportedAt: '2023-03-29T09:39:50.502Z',
 | 
					    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 manager;
 | 
				
			||||||
  let tokensContainer;
 | 
					  let tokensContainer;
 | 
				
			||||||
  const page = 'issues';
 | 
					  const page = 'issues';
 | 
				
			||||||
  const placeholder = 'Search or filter results...';
 | 
					  const placeholder = 'Search or filter results…';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function dispatchBackspaceEvent(element, eventType) {
 | 
					  function dispatchBackspaceEvent(element, eventType) {
 | 
				
			||||||
    const event = new Event(eventType);
 | 
					    const event = new Event(eventType);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,7 +137,6 @@ describe('IssuesDashboardApp component', () => {
 | 
				
			||||||
        issuablesLoading: false,
 | 
					        issuablesLoading: false,
 | 
				
			||||||
        namespace: 'dashboard',
 | 
					        namespace: 'dashboard',
 | 
				
			||||||
        recentSearchesStorageKey: 'issues',
 | 
					        recentSearchesStorageKey: 'issues',
 | 
				
			||||||
        searchInputPlaceholder: i18n.searchPlaceholder,
 | 
					 | 
				
			||||||
        showPaginationControls: true,
 | 
					        showPaginationControls: true,
 | 
				
			||||||
        sortOptions: getSortOptions({
 | 
					        sortOptions: getSortOptions({
 | 
				
			||||||
          hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
 | 
					          hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -237,7 +237,6 @@ describe('CE IssuesListApp component', () => {
 | 
				
			||||||
      expect(findIssuableList().props()).toMatchObject({
 | 
					      expect(findIssuableList().props()).toMatchObject({
 | 
				
			||||||
        namespace: defaultProvide.fullPath,
 | 
					        namespace: defaultProvide.fullPath,
 | 
				
			||||||
        recentSearchesStorageKey: 'issues',
 | 
					        recentSearchesStorageKey: 'issues',
 | 
				
			||||||
        searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
 | 
					 | 
				
			||||||
        sortOptions: getSortOptions({
 | 
					        sortOptions: getSortOptions({
 | 
				
			||||||
          hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
 | 
					          hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
 | 
				
			||||||
          hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
 | 
					          hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,7 +291,7 @@ describe('AlertManagementEmptyState', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('renders the search component for incidents', () => {
 | 
					    it('renders the search component for incidents', () => {
 | 
				
			||||||
      const filteredSearchBar = findFilteredSearchBar();
 | 
					      const filteredSearchBar = findFilteredSearchBar();
 | 
				
			||||||
      expect(filteredSearchBar.props('searchInputPlaceholder')).toBe('Search or filter results…');
 | 
					
 | 
				
			||||||
      expect(filteredSearchBar.props('tokens')).toEqual([
 | 
					      expect(filteredSearchBar.props('tokens')).toEqual([
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          type: TOKEN_TYPE_AUTHOR,
 | 
					          type: TOKEN_TYPE_AUTHOR,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { GlBreadcrumb } from '@gitlab/ui';
 | 
					import { GlBreadcrumb } from '@gitlab/ui';
 | 
				
			||||||
import { shallowMount } from '@vue/test-utils';
 | 
					 | 
				
			||||||
import { nextTick } from 'vue';
 | 
					import { nextTick } from 'vue';
 | 
				
			||||||
 | 
					import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | 
				
			||||||
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
 | 
					import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
 | 
				
			||||||
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
 | 
					import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
 | 
				
			||||||
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.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 findWelcomePage = () => wrapper.findComponent(WelcomePage);
 | 
				
			||||||
  const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
 | 
					  const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
 | 
				
			||||||
 | 
					  const findTopBar = () => wrapper.findByTestId('top-bar');
 | 
				
			||||||
  const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
 | 
					  const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
 | 
				
			||||||
  const findImage = () => wrapper.find('img');
 | 
					  const findImage = () => wrapper.find('img');
 | 
				
			||||||
  const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
 | 
					  const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
 | 
				
			||||||
| 
						 | 
					@ -30,7 +31,7 @@ describe('Experimental new namespace creation app', () => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = ({ slots, propsData } = {}) => {
 | 
					  const createComponent = ({ slots, propsData } = {}) => {
 | 
				
			||||||
    wrapper = shallowMount(NewNamespacePage, {
 | 
					    wrapper = shallowMountExtended(NewNamespacePage, {
 | 
				
			||||||
      slots,
 | 
					      slots,
 | 
				
			||||||
      propsData: {
 | 
					      propsData: {
 | 
				
			||||||
        ...DEFAULT_PROPS,
 | 
					        ...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,
 | 
					      issuablesLoading: true,
 | 
				
			||||||
      namespace: 'work-items',
 | 
					      namespace: 'work-items',
 | 
				
			||||||
      recentSearchesStorageKey: 'issues',
 | 
					      recentSearchesStorageKey: 'issues',
 | 
				
			||||||
      searchInputPlaceholder: 'Search or filter results...',
 | 
					 | 
				
			||||||
      searchTokens: [],
 | 
					      searchTokens: [],
 | 
				
			||||||
      showWorkItemTypeIcon: true,
 | 
					      showWorkItemTypeIcon: true,
 | 
				
			||||||
      sortOptions: [],
 | 
					      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
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it { is_expected.not_to include('logged-out-marketing-header') }
 | 
					    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#dispensable_render' do
 | 
					  describe '#dispensable_render' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ RSpec.describe ::API::Entities::Project do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '.service_desk_address' do
 | 
					  describe '.service_desk_address', feature_category: :service_desk do
 | 
				
			||||||
    before do
 | 
					    before do
 | 
				
			||||||
      allow(project).to receive(:service_desk_enabled?).and_return(true)
 | 
					      allow(project).to receive(:service_desk_enabled?).and_return(true)
 | 
				
			||||||
    end
 | 
					    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.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.merge_user_id',             # https://gitlab.com/gitlab-org/gitlab/-/issues/422080
 | 
				
			||||||
      'merge_requests.author_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.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
 | 
					      '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
 | 
					      '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
 | 
				
			||||||
  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) }
 | 
					    let_it_be(:project, reload: true) { create(:project, service_desk_enabled: true) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subject { project.service_desk_address }
 | 
					    subject { project.service_desk_address }
 | 
				
			||||||
| 
						 | 
					@ -2424,7 +2424,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when project_key is set' do
 | 
					      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')
 | 
					          create(:service_desk_setting, project: project, project_key: 'key1')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
 | 
					          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
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      context 'when project_key is not set' do
 | 
					      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")
 | 
					          expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '.with_service_desk_key' do
 | 
					  describe '.with_service_desk_key' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,13 +53,62 @@ RSpec.describe Admin::AbuseReportsController, type: :request, feature_category:
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  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(:report) { create(:abuse_report) }
 | 
				
			||||||
    let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
 | 
					    let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
 | 
				
			||||||
    let(:expected_params) { ActionController::Parameters.new(params).permit! }
 | 
					    let(:expected_params) { ActionController::Parameters.new(params).permit! }
 | 
				
			||||||
    let(:message) { 'Service response' }
 | 
					    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
 | 
					    it 'invokes the Admin::AbuseReports::ModerateUserService' do
 | 
				
			||||||
      expect_next_instance_of(Admin::AbuseReports::ModerateUserService, report, admin, expected_params) do |service|
 | 
					      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
 | 
				
			||||||
  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
 | 
					  describe 'DELETE #destroy' do
 | 
				
			||||||
    let!(:report) { create(:abuse_report) }
 | 
					    let!(:report) { create(:abuse_report) }
 | 
				
			||||||
    let(:params) { {} }
 | 
					    let(:params) { {} }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'spec_helper'
 | 
					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
 | 
					  include GraphqlHelpers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let_it_be(:user) { create(:user, :admin) }
 | 
					  let_it_be(:user) { create(:user, :admin) }
 | 
				
			||||||
| 
						 | 
					@ -228,7 +228,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context 'with build running', :freeze_time do
 | 
					    context 'with build running' do
 | 
				
			||||||
      let!(:pipeline) { create(:ci_pipeline, project: project1) }
 | 
					      let!(:pipeline) { create(:ci_pipeline, project: project1) }
 | 
				
			||||||
      let!(:runner_manager) do
 | 
					      let!(:runner_manager) do
 | 
				
			||||||
        create(:ci_runner_machine,
 | 
					        create(:ci_runner_machine,
 | 
				
			||||||
| 
						 | 
					@ -357,6 +357,77 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'for inactive runner' do
 | 
					  describe 'for inactive runner' do
 | 
				
			||||||
| 
						 | 
					@ -501,8 +572,14 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'for runner with status' do
 | 
					  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(:stale_runner) do
 | 
				
			||||||
    let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
 | 
					      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
 | 
					    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]
 | 
					      similar_open_report_hash = user_hash[:similar_open_reports][0]
 | 
				
			||||||
      expect(similar_open_report_hash.keys).to match_array([
 | 
					      expect(similar_open_report_hash.keys).to match_array([
 | 
				
			||||||
        :id,
 | 
					        :id,
 | 
				
			||||||
 | 
					        :global_id,
 | 
				
			||||||
        :status,
 | 
					        :status,
 | 
				
			||||||
        :message,
 | 
					        :message,
 | 
				
			||||||
        :reported_at,
 | 
					        :reported_at,
 | 
				
			||||||
| 
						 | 
					@ -100,6 +101,7 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(report_hash.keys).to match_array([
 | 
					      expect(report_hash.keys).to match_array([
 | 
				
			||||||
        :id,
 | 
					        :id,
 | 
				
			||||||
 | 
					        :global_id,
 | 
				
			||||||
        :status,
 | 
					        :status,
 | 
				
			||||||
        :message,
 | 
					        :message,
 | 
				
			||||||
        :reported_at,
 | 
					        :reported_at,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
 | 
				
			||||||
    it 'exposes correct attributes' do
 | 
					    it 'exposes correct attributes' do
 | 
				
			||||||
      expect(entity_hash.keys).to match_array([
 | 
					      expect(entity_hash.keys).to match_array([
 | 
				
			||||||
        :id,
 | 
					        :id,
 | 
				
			||||||
 | 
					        :global_id,
 | 
				
			||||||
        :status,
 | 
					        :status,
 | 
				
			||||||
        :message,
 | 
					        :message,
 | 
				
			||||||
        :reported_at,
 | 
					        :reported_at,
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,12 @@ RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat d
 | 
				
			||||||
      ])
 | 
					      ])
 | 
				
			||||||
    end
 | 
					    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
 | 
					    it 'correctly exposes `reporter`' do
 | 
				
			||||||
      reporter_hash = entity_hash[:reporter]
 | 
					      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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def default_placeholder
 | 
					  def default_placeholder
 | 
				
			||||||
    'Search or filter results...'
 | 
					    'Search or filter results…'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def get_filtered_search_placeholder
 | 
					  def get_filtered_search_placeholder
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue