Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
24e54a8f10
commit
bd28d0fa02
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import {
|
||||
GlAccordionItem,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlTokenSelector,
|
||||
GlFormCombobox,
|
||||
} from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
import { i18n } from '../constants';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
components: {
|
||||
GlAccordionItem,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlFormCombobox,
|
||||
GlTokenSelector,
|
||||
},
|
||||
props: {
|
||||
tagOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isNameValid: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isScriptValid: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['availableStages']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-accordion-item :title="$options.i18n.JOB_SETUP" visible>
|
||||
<gl-form-group
|
||||
:invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
|
||||
:state="isNameValid"
|
||||
:label="$options.i18n.JOB_NAME"
|
||||
>
|
||||
<gl-form-input
|
||||
:value="job.name"
|
||||
:state="isNameValid"
|
||||
data-testid="job-name-input"
|
||||
@input="$emit('update-job', 'name', $event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-combobox
|
||||
:value="job.stage"
|
||||
:token-list="availableStages"
|
||||
:label-text="$options.i18n.STAGE"
|
||||
data-testid="job-stage-input"
|
||||
@input="$emit('update-job', 'stage', $event)"
|
||||
/>
|
||||
<gl-form-group
|
||||
:invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
|
||||
:state="isScriptValid"
|
||||
:label="$options.i18n.SCRIPT"
|
||||
>
|
||||
<gl-form-textarea
|
||||
:value="job.script"
|
||||
:state="isScriptValid"
|
||||
:no-resize="false"
|
||||
data-testid="job-script-input"
|
||||
@input="$emit('update-job', 'script', $event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.TAGS">
|
||||
<gl-token-selector
|
||||
:dropdown-items="tagOptions"
|
||||
:selected-tokens="job.tags"
|
||||
data-testid="job-tags-input"
|
||||
@input="$emit('update-job', 'tags', $event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-accordion-item>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,38 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const DRAWER_CONTAINER_CLASS = '.content-wrapper';
|
||||
|
||||
export const JOB_TEMPLATE = {
|
||||
name: '',
|
||||
stage: '',
|
||||
script: '',
|
||||
tags: [],
|
||||
image: {
|
||||
name: '',
|
||||
entrypoint: '',
|
||||
},
|
||||
services: [
|
||||
{
|
||||
name: '',
|
||||
entrypoint: '',
|
||||
},
|
||||
],
|
||||
artifacts: {
|
||||
paths: [''],
|
||||
exclude: [''],
|
||||
},
|
||||
cache: {
|
||||
paths: [''],
|
||||
key: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const i18n = {
|
||||
ADD_JOB: s__('JobAssistant|Add job'),
|
||||
SCRIPT: s__('JobAssistant|Script'),
|
||||
JOB_NAME: s__('JobAssistant|Job name'),
|
||||
JOB_SETUP: s__('JobAssistant|Job Setup'),
|
||||
STAGE: s__('JobAssistant|Stage (optional)'),
|
||||
TAGS: s__('JobAssistant|Tags (optional)'),
|
||||
THIS_FIELD_IS_REQUIRED: __('This field is required'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
<script>
|
||||
import { GlDrawer, GlButton } from '@gitlab/ui';
|
||||
import { GlDrawer, GlAccordion, GlButton } from '@gitlab/ui';
|
||||
import { stringify } from 'yaml';
|
||||
import { mapMutations, mapState } from 'vuex';
|
||||
import { set, omit, trim } from 'lodash';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { DRAWER_CONTAINER_CLASS, i18n } from './constants';
|
||||
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
|
||||
import { UPDATE_CI_CONFIG } from '~/ci/pipeline_editor/store/mutation_types';
|
||||
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
|
||||
import { DRAWER_CONTAINER_CLASS, JOB_TEMPLATE, i18n } from './constants';
|
||||
import { removeEmptyObj, trimFields } from './utils';
|
||||
import JobSetupItem from './accordion_items/job_setup_item.vue';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
components: {
|
||||
GlDrawer,
|
||||
GlAccordion,
|
||||
GlButton,
|
||||
JobSetupItem,
|
||||
},
|
||||
props: {
|
||||
isVisible: {
|
||||
|
|
@ -21,15 +31,84 @@ export default {
|
|||
default: 200,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNameValid: true,
|
||||
isScriptValid: true,
|
||||
job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
runners: {
|
||||
query: getAllRunners,
|
||||
update(data) {
|
||||
return data?.runners?.nodes || [];
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentCiFileContent']),
|
||||
tagOptions() {
|
||||
const options = [];
|
||||
this.runners?.forEach((runner) => options.push(...runner.tagList));
|
||||
return [...new Set(options)].map((tag) => {
|
||||
return {
|
||||
id: tag,
|
||||
name: tag,
|
||||
};
|
||||
});
|
||||
},
|
||||
drawerHeightOffset() {
|
||||
return getContentWrapperHeight(DRAWER_CONTAINER_CLASS);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
updateCiConfig: UPDATE_CI_CONFIG,
|
||||
}),
|
||||
closeDrawer() {
|
||||
this.clearJob();
|
||||
this.$emit('close-job-assistant-drawer');
|
||||
},
|
||||
addCiConfig() {
|
||||
this.isNameValid = this.validate(this.job.name);
|
||||
this.isScriptValid = this.validate(this.job.script);
|
||||
|
||||
if (!this.isNameValid || !this.isScriptValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newJobString = this.generateYmlString();
|
||||
this.updateCiConfig(`${this.currentCiFileContent}\n${newJobString}`);
|
||||
eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
|
||||
|
||||
this.closeDrawer();
|
||||
},
|
||||
generateYmlString() {
|
||||
let job = JSON.parse(JSON.stringify(this.job));
|
||||
const jobName = job.name;
|
||||
job = omit(job, ['name']);
|
||||
job.tags = job.tags.map((tag) => tag.name); // Tag item is originally an option object, we need a string here to match `.gitlab-ci.yml` rules
|
||||
const cleanedJob = trimFields(removeEmptyObj(job));
|
||||
return stringify({ [jobName]: cleanedJob });
|
||||
},
|
||||
clearJob() {
|
||||
this.job = JSON.parse(JSON.stringify(JOB_TEMPLATE));
|
||||
this.isNameValid = true;
|
||||
this.isScriptValid = true;
|
||||
},
|
||||
updateJob(key, value) {
|
||||
set(this.job, key, value);
|
||||
if (key === 'name') {
|
||||
this.isNameValid = this.validate(this.job.name);
|
||||
}
|
||||
if (key === 'script') {
|
||||
this.isScriptValid = this.validate(this.job.script);
|
||||
}
|
||||
},
|
||||
validate(value) {
|
||||
return trim(value) !== '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -44,6 +123,15 @@ export default {
|
|||
<template #title>
|
||||
<h2 class="gl-m-0 gl-font-lg">{{ $options.i18n.ADD_JOB }}</h2>
|
||||
</template>
|
||||
<gl-accordion :header-level="3">
|
||||
<job-setup-item
|
||||
:tag-options="tagOptions"
|
||||
:job="job"
|
||||
:is-name-valid="isNameValid"
|
||||
:is-script-valid="isScriptValid"
|
||||
@update-job="updateJob"
|
||||
/>
|
||||
</gl-accordion>
|
||||
<template #footer>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
|
|
@ -51,11 +139,15 @@ export default {
|
|||
class="gl-mr-3"
|
||||
data-testid="cancel-button"
|
||||
@click="closeDrawer"
|
||||
>{{ __('Cancel') }}</gl-button
|
||||
>
|
||||
<gl-button category="primary" variant="confirm" data-testid="confirm-button">{{
|
||||
__('Add')
|
||||
}}</gl-button>
|
||||
>{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
data-testid="confirm-button"
|
||||
@click="addCiConfig"
|
||||
>{{ __('Add') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { isEmpty, isObject, isArray, isString, reject, omitBy, mapValues, map, trim } from 'lodash';
|
||||
|
||||
const isEmptyValue = (val) => (isObject(val) || isString(val)) && isEmpty(val);
|
||||
const trimText = (val) => (isString(val) ? trim(val) : val);
|
||||
|
||||
export const removeEmptyObj = (obj) => {
|
||||
if (isArray(obj)) {
|
||||
return reject(map(obj, removeEmptyObj), isEmptyValue);
|
||||
} else if (isObject(obj)) {
|
||||
return omitBy(mapValues(obj, removeEmptyObj), isEmptyValue);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const trimFields = (data) => {
|
||||
if (isArray(data)) {
|
||||
return data.map(trimFields);
|
||||
} else if (isObject(data)) {
|
||||
return mapValues(data, trimFields);
|
||||
}
|
||||
return trimText(data);
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default createEventHub();
|
||||
|
||||
export const SCROLL_EDITOR_TO_BOTTOM = Symbol('scrollEditorToBottom');
|
||||
|
|
@ -12,6 +12,7 @@ import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphq
|
|||
import { resolvers } from './graphql/resolvers';
|
||||
import typeDefs from './graphql/typedefs.graphql';
|
||||
import PipelineEditorApp from './pipeline_editor_app.vue';
|
||||
import createStore from './store';
|
||||
|
||||
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
||||
const el = document.querySelector(selector);
|
||||
|
|
@ -111,8 +112,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
|||
},
|
||||
});
|
||||
|
||||
const store = createStore();
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
ciConfigPath,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
import { parse } from 'yaml';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
|
||||
import { UPDATE_CI_CONFIG, UPDATE_AVAILABLE_STAGES } from './store/mutation_types';
|
||||
|
||||
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
|
||||
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
|
||||
|
|
@ -44,7 +46,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
ciConfigData: {},
|
||||
currentCiFileContent: '',
|
||||
failureType: null,
|
||||
failureReasons: [],
|
||||
hasBranchLoaded: false,
|
||||
|
|
@ -94,7 +95,7 @@ export default {
|
|||
const fileContent = rawBlob ?? '';
|
||||
|
||||
this.lastCommittedContent = fileContent;
|
||||
this.currentCiFileContent = fileContent;
|
||||
this.updateCiConfig(fileContent);
|
||||
|
||||
// If rawBlob is defined and returns a string, it means that there is
|
||||
// a CI config file with empty content. If `rawBlob` is not defined
|
||||
|
|
@ -155,6 +156,10 @@ export default {
|
|||
this.isLintUnavailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.ciConfig?.mergedYaml) {
|
||||
this.updateAvailableStages(parse(data.ciConfig.mergedYaml).stages);
|
||||
}
|
||||
},
|
||||
error() {
|
||||
// We are not using `reportFailure` here because we don't
|
||||
|
|
@ -231,6 +236,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentCiFileContent']),
|
||||
hasUnsavedChanges() {
|
||||
return this.lastCommittedContent !== this.currentCiFileContent;
|
||||
},
|
||||
|
|
@ -294,6 +300,10 @@ export default {
|
|||
this.checkShouldSkipStartScreen();
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
updateCiConfig: UPDATE_CI_CONFIG,
|
||||
updateAvailableStages: UPDATE_AVAILABLE_STAGES,
|
||||
}),
|
||||
checkShouldSkipStartScreen() {
|
||||
const params = queryToObject(window.location.search);
|
||||
this.shouldSkipStartScreen = Boolean(params?.add_new_config_file);
|
||||
|
|
@ -344,7 +354,7 @@ export default {
|
|||
},
|
||||
resetContent() {
|
||||
this.showResetConfirmationModal = false;
|
||||
this.currentCiFileContent = this.lastCommittedContent;
|
||||
this.updateCiConfig(this.lastCommittedContent);
|
||||
},
|
||||
setAppStatus(appStatus) {
|
||||
if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
|
||||
|
|
@ -361,9 +371,6 @@ export default {
|
|||
showErrorAlert({ type, reasons = [] }) {
|
||||
this.reportFailure(type, reasons);
|
||||
},
|
||||
updateCiConfig(ciFileContent) {
|
||||
this.currentCiFileContent = ciFileContent;
|
||||
},
|
||||
updateCommitSha() {
|
||||
this.isFetchingCommitSha = true;
|
||||
this.$apollo.queries.commitSha.refetch();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default () =>
|
||||
new Vuex.Store({
|
||||
mutations,
|
||||
state: state(),
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export const UPDATE_CI_CONFIG = 'UPDATE_CI_CONFIG';
|
||||
export const UPDATE_AVAILABLE_STAGES = 'UPDATE_AVAILABLE_STAGES';
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.UPDATE_CI_CONFIG](state, content) {
|
||||
state.currentCiFileContent = content;
|
||||
},
|
||||
[types.UPDATE_AVAILABLE_STAGES](state, stages) {
|
||||
state.availableStages = stages || [];
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export default () => ({
|
||||
currentCiFileContent: '',
|
||||
availableStages: [],
|
||||
});
|
||||
|
|
@ -1,18 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AbuseReportsFinder
|
||||
attr_reader :params
|
||||
attr_reader :params, :reports
|
||||
|
||||
def initialize(params = {})
|
||||
@params = params
|
||||
@reports = AbuseReport.all
|
||||
end
|
||||
|
||||
def execute
|
||||
reports = AbuseReport.all
|
||||
reports = reports.by_user(params[:user_id]) if params[:user_id].present?
|
||||
filter_reports
|
||||
|
||||
reports.with_order_id_desc
|
||||
.with_users
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_reports
|
||||
filter_by_user_id
|
||||
|
||||
filter_by_status
|
||||
filter_by_category
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
return unless params[:status].present?
|
||||
|
||||
case params[:status]
|
||||
when 'open'
|
||||
@reports = @reports.open
|
||||
when 'closed'
|
||||
@reports = @reports.closed
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_category
|
||||
return unless params[:category].present?
|
||||
|
||||
@reports = @reports.by_category(params[:category])
|
||||
end
|
||||
|
||||
def filter_by_user_id
|
||||
return unless params[:user_id].present?
|
||||
|
||||
@reports = @reports.by_user_id(params[:user_id])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ class AbuseReport < ApplicationRecord
|
|||
before_validation :filter_empty_strings_from_links_to_spam
|
||||
validate :links_to_spam_contains_valid_urls
|
||||
|
||||
scope :by_user, ->(user) { where(user_id: user) }
|
||||
scope :by_user_id, ->(id) { where(user_id: id) }
|
||||
scope :by_category, ->(category) { where(category: category) }
|
||||
scope :with_users, -> { includes(:reporter, :user) }
|
||||
|
||||
enum category: {
|
||||
|
|
@ -56,6 +57,11 @@ class AbuseReport < ApplicationRecord
|
|||
other: 8
|
||||
}
|
||||
|
||||
enum status: {
|
||||
open: 1,
|
||||
closed: 2
|
||||
}
|
||||
|
||||
# For CacheMarkdownField
|
||||
alias_method :author, :reporter
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AbuseReportEntity < Grape::Entity
|
||||
expose :category
|
||||
expose :updated_at
|
||||
|
||||
expose :reported_user do |report|
|
||||
UserEntity.represent(report.user, only: [:name])
|
||||
end
|
||||
|
||||
expose :reporter do |report|
|
||||
UserEntity.represent(report.reporter, only: [:name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AbuseReportSerializer < BaseSerializer
|
||||
entity Admin::AbuseReportEntity
|
||||
end
|
||||
end
|
||||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
module Issues
|
||||
class AfterCreateService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(issue)
|
||||
todo_service.new_issue(issue, current_user)
|
||||
delete_milestone_total_issue_counter_cache(issue.milestone)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ module Issues
|
|||
include IncidentManagement::UsageData
|
||||
include IssueTypeHelpers
|
||||
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def hook_data(issue, action, old_associations: {})
|
||||
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
|
||||
hook_data[:object_attributes][:action] = action
|
||||
|
|
@ -33,6 +37,14 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
# overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
|
||||
# Issues::ReopenService constructor signature is different now, it takes container instead of project also
|
||||
# IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
|
||||
# MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
|
||||
def self.constructor_container_arg(value)
|
||||
{ container: value }
|
||||
end
|
||||
|
||||
def find_work_item_type_id(issue_type)
|
||||
work_item_type = WorkItems::Type.default_by_type(issue_type)
|
||||
work_item_type ||= WorkItems::Type.default_issue_type
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ module Issues
|
|||
class BuildService < Issues::BaseService
|
||||
include ResolveDiscussions
|
||||
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute
|
||||
filter_resolve_discussion_params
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
module Issues
|
||||
class CloseService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
# Closes the supplied issue if the current user is able to do so.
|
||||
def execute(issue, commit: nil, notifications: true, system_note: true, skip_authorization: false)
|
||||
return issue unless can_close?(issue, skip_authorization: skip_authorization)
|
||||
|
|
@ -56,11 +51,6 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
# TODO: remove once MergeRequests::CloseService or IssuableBaseService method is changed.
|
||||
def self.constructor_container_arg(value)
|
||||
{ container: value }
|
||||
end
|
||||
|
||||
def can_close?(issue, skip_authorization: false)
|
||||
skip_authorization || can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ module Issues
|
|||
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
|
||||
def initialize(container:, spam_params:, current_user: nil, params: {}, build_service: nil)
|
||||
@extra_params = params.delete(:extra_params) || {}
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
super(container: container, current_user: current_user, params: params)
|
||||
@spam_params = spam_params
|
||||
@build_service = build_service || BuildService.new(container: project, current_user: current_user, params: params)
|
||||
@build_service = build_service ||
|
||||
BuildService.new(container: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(skip_system_notes: false)
|
||||
|
|
@ -100,10 +101,6 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
def self.constructor_container_arg(value)
|
||||
{ container: value }
|
||||
end
|
||||
|
||||
def handle_quick_actions(issue)
|
||||
# Do not handle quick actions unless the work item is the default Issue.
|
||||
# The available quick actions for a work item depend on its type and widgets.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
module Issues
|
||||
class DuplicateService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(duplicate_issue, canonical_issue)
|
||||
return if canonical_issue == duplicate_issue
|
||||
return unless can?(current_user, :update_issue, duplicate_issue)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
module Issues
|
||||
class ReferencedMergeRequestsService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def execute(issue)
|
||||
referenced = referenced_merge_requests(issue)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@
|
|||
# those with a merge request open referencing the current issue.
|
||||
module Issues
|
||||
class RelatedBranchesService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(issue)
|
||||
branch_names_with_mrs = branches_with_merge_request_for(issue)
|
||||
branches = branches_with_iid_of(issue).reject { |b| branch_names_with_mrs.include?(b[:name]) }
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
module Issues
|
||||
class ReopenService < Issues::BaseService
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(issue, skip_authorization: false)
|
||||
return issue unless can_reopen?(issue, skip_authorization: skip_authorization)
|
||||
|
||||
|
|
@ -27,14 +22,6 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
# overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
|
||||
# Issues::ReopenService constructor signature is different now, it takes container instead of project also
|
||||
# IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
|
||||
# MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
|
||||
def self.constructor_container_arg(value)
|
||||
{ container: value }
|
||||
end
|
||||
|
||||
def can_reopen?(issue, skip_authorization: false)
|
||||
skip_authorization || can?(current_user, :reopen_issue, issue)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ module Issues
|
|||
class ReorderService < Issues::BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
end
|
||||
|
||||
def execute(issue)
|
||||
return false unless can?(current_user, :update_issue, issue)
|
||||
return false unless move_between_ids
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Issues
|
|||
# necessary in many cases, and we don't want to require every caller to explicitly pass it as nil
|
||||
# to disable spam checking.
|
||||
def initialize(container:, current_user: nil, params: {}, spam_params: nil)
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
super(container: container, current_user: current_user, params: params)
|
||||
@spam_params = spam_params
|
||||
end
|
||||
|
||||
|
|
@ -116,15 +116,6 @@ module Issues
|
|||
|
||||
attr_reader :spam_params
|
||||
|
||||
# TODO: remove this once MergeRequests::UpdateService#initialize is changed to take container as named argument.
|
||||
#
|
||||
# Issues::UpdateService is used together with MergeRequests::UpdateService in Mutations::Assignable#assign! method
|
||||
# however MergeRequests::UpdateService#initialize still takes `project` as param and Issues::UpdateService is being
|
||||
# changed to take `container` as param. So we are adding this workaround in the meantime.
|
||||
def self.constructor_container_arg(value)
|
||||
{ container: value }
|
||||
end
|
||||
|
||||
def handle_quick_actions(issue)
|
||||
# Do not handle quick actions unless the work item is the default Issue.
|
||||
# The available quick actions for a work item depend on its type and widgets.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
module Issues
|
||||
class ZoomLinkService < Issues::BaseService
|
||||
def initialize(container:, current_user:, params:)
|
||||
super(project: container, current_user: current_user, params: params)
|
||||
super
|
||||
|
||||
@issue = params.fetch(:issue)
|
||||
@added_meeting = ZoomMeeting.canonical_meeting(@issue)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ module Issues
|
|||
leftover = to_place.pop if to_place.count > QUERY_LIMIT
|
||||
|
||||
Issue.move_nulls_to_end(to_place)
|
||||
Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
|
||||
Issues::BaseService.new(container: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
|
||||
Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
|
||||
rescue RelativePositioning::NoSpaceLeft => e
|
||||
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStatusAndResolvedAtToAbuseReports < Gitlab::Database::Migration[2.1]
|
||||
def change
|
||||
add_column :abuse_reports, :status, :integer, limit: 2, default: 1, null: false
|
||||
add_timestamps_with_timezone(:abuse_reports, columns: [:resolved_at], null: true)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStatusCategoryAndIdIndexToAbuseReports < Gitlab::Database::Migration[2.1]
|
||||
INDEX_NAME = 'index_abuse_reports_on_status_category_and_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :abuse_reports, [:status, :category, :id], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :abuse_reports, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStatusAndIdIndexToAbuseReports < Gitlab::Database::Migration[2.1]
|
||||
INDEX_NAME = 'index_abuse_reports_on_status_and_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :abuse_reports, [:status, :id], name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :abuse_reports, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexForNextOverLimitCheckAt < Gitlab::Database::Migration[2.1]
|
||||
TABLE_NAME = 'namespace_details'
|
||||
INDEX_NAME = 'index_next_over_limit_check_at_asc_order'
|
||||
|
||||
def up
|
||||
prepare_async_index TABLE_NAME,
|
||||
:next_over_limit_check_at,
|
||||
order: { next_over_limit_check_at: 'ASC NULLS FIRST' },
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index TABLE_NAME, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
23979065610c4f361a639cdcf81e7ce491d111ed3752bd11081f9645b31e21f6
|
||||
|
|
@ -0,0 +1 @@
|
|||
c6a905e29792b88f87810d267a4472886e0a1a22fe9531e3d7998abbd1035552
|
||||
|
|
@ -0,0 +1 @@
|
|||
204503fcf9e5da7255677a9a82f11e860410048efc1ed75cc7ba97b3cdd273c3
|
||||
|
|
@ -0,0 +1 @@
|
|||
f5636e464b16bfc201a3f3a21269c6d8686d2bc829aa80491bea120fd10e138a
|
||||
|
|
@ -10733,6 +10733,8 @@ CREATE TABLE abuse_reports (
|
|||
category smallint DEFAULT 1 NOT NULL,
|
||||
reported_from_url text DEFAULT ''::text NOT NULL,
|
||||
links_to_spam text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
status smallint DEFAULT 1 NOT NULL,
|
||||
resolved_at timestamp with time zone,
|
||||
CONSTRAINT abuse_reports_links_to_spam_length_check CHECK ((cardinality(links_to_spam) <= 20)),
|
||||
CONSTRAINT check_ab1260fa6c CHECK ((char_length(reported_from_url) <= 512))
|
||||
);
|
||||
|
|
@ -28991,6 +28993,10 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
|
|||
|
||||
CREATE UNIQUE INDEX idx_work_item_types_on_namespace_id_and_name_null_namespace ON work_item_types USING btree (btrim(lower(name)), ((namespace_id IS NULL))) WHERE (namespace_id IS NULL);
|
||||
|
||||
CREATE INDEX index_abuse_reports_on_status_and_id ON abuse_reports USING btree (status, id);
|
||||
|
||||
CREATE INDEX index_abuse_reports_on_status_category_and_id ON abuse_reports USING btree (status, category, id);
|
||||
|
||||
CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id);
|
||||
|
||||
CREATE UNIQUE INDEX "index_achievements_on_namespace_id_LOWER_name" ON achievements USING btree (namespace_id, lower(name));
|
||||
|
|
|
|||
|
|
@ -159,12 +159,18 @@ do not change. This method is called *merging*.
|
|||
|
||||
### Merge method for `include`
|
||||
|
||||
For a file containing `include` directives, the included files are read in order (possibly
|
||||
recursively), and the configuration in these files is likewise merged in order. If the parameters overlap, the last included file takes precedence. Finally, the directives in the
|
||||
file itself are merged with the configuration from the included files.
|
||||
The `include` configuration merges with the main configuration file with this process:
|
||||
|
||||
- Included files are read in the order defined in the configuration file, and
|
||||
the included configuration is merged together in the same order.
|
||||
- If an included file also uses `include`, that nested `include` configuration is merged first (recursively).
|
||||
- If parameters overlap, the last included file takes precedence when merging the configuration
|
||||
from the included files.
|
||||
- After all configuration added with `include` is merged together, the main configuration
|
||||
is merged with the included configuration.
|
||||
|
||||
This merge method is a _deep merge_, where hash maps are merged at any depth in the
|
||||
configuration. To merge hash map A (containing the configuration merged so far) and B (the next piece
|
||||
configuration. To merge hash map "A" (that contains the configuration merged so far) and "B" (the next piece
|
||||
of configuration), the keys and values are processed as follows:
|
||||
|
||||
- When the key only exists in A, use the key and value from A.
|
||||
|
|
@ -172,9 +178,7 @@ of configuration), the keys and values are processed as follows:
|
|||
- When the key exists in both A and B, and one of the values is not a hash map, use the value from B.
|
||||
- Otherwise, use the key and value from B.
|
||||
|
||||
For example:
|
||||
|
||||
We have a configuration consisting of two files.
|
||||
For example, with a configuration that consists of two files:
|
||||
|
||||
- The `.gitlab-ci.yml` file:
|
||||
|
||||
|
|
@ -211,7 +215,7 @@ We have a configuration consisting of two files.
|
|||
dotenv: deploy.env
|
||||
```
|
||||
|
||||
The merged result:
|
||||
The merged result is:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
|
|
|
|||
|
|
@ -1314,7 +1314,10 @@ See also [downgrade](#downgrade) and [roll back](#roll-back).
|
|||
|
||||
## upper left, upper right
|
||||
|
||||
Use **upper left** and **upper right** instead of **top left** and **top right**. Hyphenate as adjectives (for example, **upper-left corner**).
|
||||
Use **upper-left corner** and **upper-right corner** to provide direction in the UI.
|
||||
If the UI element is not in a corner, use **upper left** and **upper right**.
|
||||
|
||||
Do not use **top left** and **top right**.
|
||||
|
||||
For details, see the [Microsoft style guide](https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/u/upper-left-upper-right).
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,22 @@ For example, `Create an issue`.
|
|||
If several tasks on a page share prerequisites, you can create a separate
|
||||
topic with the title `Prerequisites`.
|
||||
|
||||
## When a task has only one step
|
||||
|
||||
If you need to write a task that has only one step, make that step an unordered list item.
|
||||
This format helps the step stand out, while keeping it consistent with the rules
|
||||
for lists.
|
||||
|
||||
For example:
|
||||
|
||||
```markdown
|
||||
# Create a merge request
|
||||
|
||||
To create a merge request:
|
||||
|
||||
- In the upper-right corner, select **New merge request**.
|
||||
```
|
||||
|
||||
### When more than one way exists to perform a task
|
||||
|
||||
If more than one way exists to perform a task in the UI, you should
|
||||
|
|
|
|||
|
|
@ -129,19 +129,6 @@ time that the first policy merge request is created.
|
|||
|
||||
You can use the [Vulnerability-Check Migration](https://gitlab.com/gitlab-org/gitlab/-/snippets/2328089) script to bulk create policies or associate security policy projects with development projects. For instructions and a demonstration of how to use the Vulnerability-Check Migration script, see [this video](https://youtu.be/biU1N26DfBc).
|
||||
|
||||
## Scan execution policies
|
||||
|
||||
See [Scan execution policies](scan-execution-policies.md).
|
||||
|
||||
## Scan result policy editor
|
||||
|
||||
See [Scan result policies](scan-result-policies.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
See the [Category Direction page](https://about.gitlab.com/direction/govern/security_policies/security_policy_management/)
|
||||
for more information on the product direction of security policies within GitLab.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `Branch name 'update-policy-<timestamp>' does not follow the pattern '<branch_name_regex>'`
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ module API
|
|||
module Entities
|
||||
# Use with care, this exposes the secret
|
||||
class ApplicationWithSecret < Entities::Application
|
||||
expose :secret, documentation: { type: 'string',
|
||||
example: 'ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34' }
|
||||
expose :secret, documentation: {
|
||||
type: 'string',
|
||||
example: 'ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34'
|
||||
} do |application, _options|
|
||||
application.plaintext_secret
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24326,9 +24326,24 @@ msgstr ""
|
|||
msgid "JobAssistant|Add job"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Job Setup"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Job assistant"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Job name"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Script"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Stage (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "JobAssistant|Tags (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 9', require: 'gitlab/qa'
|
||||
gem 'gitlab-qa', '~> 9', '>= 9.1.0', require: 'gitlab/qa'
|
||||
gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.20.0'
|
||||
gem 'capybara', '~> 3.38.0'
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ GEM
|
|||
gitlab (4.18.0)
|
||||
httparty (~> 0.18)
|
||||
terminal-table (>= 1.5.1)
|
||||
gitlab-qa (9.0.0)
|
||||
gitlab-qa (9.1.0)
|
||||
activesupport (~> 6.1)
|
||||
gitlab (~> 4.18.0)
|
||||
http (~> 5.0)
|
||||
|
|
@ -317,7 +317,7 @@ DEPENDENCIES
|
|||
faraday-retry (~> 2.0)
|
||||
fog-core (= 2.1.0)
|
||||
fog-google (~> 1.19)
|
||||
gitlab-qa (~> 9)
|
||||
gitlab-qa (~> 9, >= 9.1.0)
|
||||
influxdb-client (~> 2.9)
|
||||
knapsack (~> 4.0)
|
||||
nokogiri (~> 1.14, >= 1.14.2)
|
||||
|
|
|
|||
|
|
@ -31,10 +31,14 @@ module QA
|
|||
parse_body(api_get_from("#{api_members_path}/all"))
|
||||
end
|
||||
|
||||
def find_member(username)
|
||||
def find_direct_member(username)
|
||||
list_members.find { |member| member[:username] == username }
|
||||
end
|
||||
|
||||
def find_direct_or_inherited_member(username)
|
||||
list_all_members.find { |member| member[:username] == username }
|
||||
end
|
||||
|
||||
def invite_group(group, access_level = AccessLevel::GUEST)
|
||||
Support::Retrier.retry_until do
|
||||
QA::Runtime::Logger.info(%(Sharing #{self.class.name} with #{group.name}))
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ module QA
|
|||
|
||||
before do
|
||||
parent_group.add_member(parent_group_user)
|
||||
|
||||
# Due to the async nature of project authorization refreshes,
|
||||
# we wait to confirm the user has been added as a member and
|
||||
# their access level has been updated before proceeding with the test
|
||||
wait_for_membership_update(parent_group_user, sub_group_project, Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
|
||||
it(
|
||||
|
|
@ -177,6 +182,16 @@ module QA
|
|||
sub_group_user.remove_via_api!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wait_for_membership_update(user, project, access_level)
|
||||
Support::Retrier.retry_until(sleep_interval: 1, message: 'Waiting for user membership to be updated') do
|
||||
found_member = project.reload!.find_direct_or_inherited_member(user.username)
|
||||
|
||||
found_member && found_member.fetch(:access_level) == access_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ module QA
|
|||
|
||||
it 'adds user to the group',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/386792' do
|
||||
found_member = group.reload!.find_member(user.username)
|
||||
found_member = group.reload!.find_direct_member(user.username)
|
||||
|
||||
expect(found_member).not_to be_nil
|
||||
expect(found_member.fetch(:access_level))
|
||||
|
|
@ -82,7 +82,7 @@ module QA
|
|||
|
||||
it 'does not add user to the group',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/386793' do
|
||||
found_member = group.reload!.find_member(user.username)
|
||||
found_member = group.reload!.find_direct_member(user.username)
|
||||
|
||||
expect(found_member).to be_nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,5 +7,9 @@ FactoryBot.define do
|
|||
message { 'User sends spam' }
|
||||
reported_from_url { 'http://gitlab.com' }
|
||||
links_to_spam { ['https://gitlab.com/issue1', 'https://gitlab.com/issue2'] }
|
||||
|
||||
trait :closed do
|
||||
status { 'closed' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,22 +6,48 @@ RSpec.describe AbuseReportsFinder, '#execute' do
|
|||
let(:params) { {} }
|
||||
let!(:user1) { create(:user) }
|
||||
let!(:user2) { create(:user) }
|
||||
let!(:abuse_report_1) { create(:abuse_report, user: user1) }
|
||||
let!(:abuse_report_2) { create(:abuse_report, user: user2) }
|
||||
let!(:abuse_report_1) { create(:abuse_report, category: 'spam', user: user1, reporter: user2) }
|
||||
let!(:abuse_report_2) { create(:abuse_report, :closed, category: 'phishing', user: user2) }
|
||||
|
||||
subject { described_class.new(params).execute }
|
||||
|
||||
context 'empty params' do
|
||||
context 'when params is empty' do
|
||||
it 'returns all abuse reports' do
|
||||
expect(subject).to match_array([abuse_report_1, abuse_report_2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'params[:user_id] is present' do
|
||||
context 'when params[:user_id] is present' do
|
||||
let(:params) { { user_id: user2 } }
|
||||
|
||||
it 'returns abuse reports for the specified user' do
|
||||
expect(subject).to match_array([abuse_report_2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when params[:status] is present' do
|
||||
context 'when value is "open"' do
|
||||
let(:params) { { status: 'open' } }
|
||||
|
||||
it 'returns only open abuse reports' do
|
||||
expect(subject).to match_array([abuse_report_1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when value is "closed"' do
|
||||
let(:params) { { status: 'closed' } }
|
||||
|
||||
it 'returns only closed abuse reports' do
|
||||
expect(subject).to match_array([abuse_report_2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when params[:category] is present' do
|
||||
let(:params) { { category: 'phishing' } }
|
||||
|
||||
it 'returns abuse reports with the specified category' do
|
||||
expect(subject).to match_array([abuse_report_2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import createStore from '~/ci/pipeline_editor/store';
|
||||
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
|
||||
|
||||
describe('Job setup item', () => {
|
||||
let wrapper;
|
||||
|
||||
const findJobNameInput = () => wrapper.findByTestId('job-name-input');
|
||||
const findJobScriptInput = () => wrapper.findByTestId('job-script-input');
|
||||
const findJobTagsInput = () => wrapper.findByTestId('job-tags-input');
|
||||
const findJobStageInput = () => wrapper.findByTestId('job-stage-input');
|
||||
|
||||
const dummyJobName = 'dummyJobName';
|
||||
const dummyJobScript = 'dummyJobScript';
|
||||
const dummyJobStage = 'dummyJobStage';
|
||||
const dummyJobTags = ['tag1'];
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(JobSetupItem, {
|
||||
store: createStore(),
|
||||
propsData: {
|
||||
tagOptions: [
|
||||
{ id: 'tag1', name: 'tag1' },
|
||||
{ id: 'tag2', name: 'tag2' },
|
||||
],
|
||||
isNameValid: true,
|
||||
isScriptValid: true,
|
||||
job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should emit update job event when filling inputs', () => {
|
||||
expect(wrapper.emitted('update-job')).toBeUndefined();
|
||||
|
||||
findJobNameInput().vm.$emit('input', dummyJobName);
|
||||
|
||||
expect(wrapper.emitted('update-job')).toHaveLength(1);
|
||||
expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]);
|
||||
|
||||
findJobScriptInput().vm.$emit('input', dummyJobScript);
|
||||
|
||||
expect(wrapper.emitted('update-job')).toHaveLength(2);
|
||||
expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]);
|
||||
|
||||
findJobStageInput().vm.$emit('input', dummyJobStage);
|
||||
|
||||
expect(wrapper.emitted('update-job')).toHaveLength(3);
|
||||
expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]);
|
||||
|
||||
findJobTagsInput().vm.$emit('input', dummyJobTags);
|
||||
|
||||
expect(wrapper.emitted('update-job')).toHaveLength(4);
|
||||
expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,24 +1,43 @@
|
|||
import { GlDrawer } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { stringify } from 'yaml';
|
||||
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
|
||||
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
|
||||
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createStore from '~/ci/pipeline_editor/store';
|
||||
import { mockAllRunnersQueryResponse } from 'jest/ci/pipeline_editor/mock_data';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Job assistant drawer', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const dummyJobName = 'a';
|
||||
const dummyJobScript = 'b';
|
||||
|
||||
const findDrawer = () => wrapper.findComponent(GlDrawer);
|
||||
const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
|
||||
|
||||
const findConfirmButton = () => wrapper.findByTestId('confirm-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||
|
||||
const createComponent = () => {
|
||||
mockApollo = createMockApollo([
|
||||
[getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)],
|
||||
]);
|
||||
|
||||
wrapper = mountExtended(JobAssistantDrawer, {
|
||||
store: createStore(),
|
||||
propsData: {
|
||||
isVisible: true,
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -42,4 +61,69 @@ describe('Job assistant drawer', () => {
|
|||
|
||||
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('trigger validate if job name is empty', async () => {
|
||||
const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
|
||||
findJobSetupItem().vm.$emit('update-job', 'script', 'b');
|
||||
findConfirmButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findJobSetupItem().props('isNameValid')).toBe(false);
|
||||
expect(findJobSetupItem().props('isScriptValid')).toBe(true);
|
||||
expect(updateCiConfigSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('when enter valid input', () => {
|
||||
beforeEach(() => {
|
||||
findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName);
|
||||
findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
|
||||
});
|
||||
|
||||
it('job name and script have correct value', () => {
|
||||
expect(findJobSetupItem().props('job')).toMatchObject({
|
||||
name: dummyJobName,
|
||||
script: dummyJobScript,
|
||||
});
|
||||
});
|
||||
|
||||
it('job name and script state should be valid', () => {
|
||||
expect(findJobSetupItem().props('isNameValid')).toBe(true);
|
||||
expect(findJobSetupItem().props('isScriptValid')).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear job data when click confirm button', async () => {
|
||||
findConfirmButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
|
||||
});
|
||||
|
||||
it('should clear job data when click cancel button', async () => {
|
||||
findCancelButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
|
||||
});
|
||||
|
||||
it('should update correct ci content when click add button', () => {
|
||||
const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
|
||||
|
||||
findConfirmButton().trigger('click');
|
||||
|
||||
expect(updateCiConfigSpy).toHaveBeenCalledWith(
|
||||
`\n${stringify({ [dummyJobName]: { script: dummyJobScript } })}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit scroll editor to button event when click add button', () => {
|
||||
const eventHubSpy = jest.spyOn(eventHub, '$emit');
|
||||
|
||||
findConfirmButton().trigger('click');
|
||||
|
||||
expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -583,6 +583,91 @@ export const mockCommitCreateResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockAllRunnersQueryResponse = {
|
||||
data: {
|
||||
runners: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/1',
|
||||
description: 'test',
|
||||
runnerType: 'PROJECT_TYPE',
|
||||
shortSha: 'DdTYMQGS',
|
||||
version: '15.6.1',
|
||||
ipAddress: '127.0.0.1',
|
||||
active: true,
|
||||
locked: true,
|
||||
jobCount: 0,
|
||||
jobExecutionStatus: 'IDLE',
|
||||
tagList: ['tag1', 'tag2', 'tag3'],
|
||||
createdAt: '2022-11-29T09:37:43Z',
|
||||
contactedAt: null,
|
||||
status: 'NEVER_CONTACTED',
|
||||
userPermissions: {
|
||||
updateRunner: true,
|
||||
deleteRunner: true,
|
||||
__typename: 'RunnerPermissions',
|
||||
},
|
||||
groups: null,
|
||||
ownerProject: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
name: '123',
|
||||
nameWithNamespace: 'Administrator / 123',
|
||||
webUrl: 'http://127.0.0.1:3000/root/test',
|
||||
__typename: 'Project',
|
||||
},
|
||||
__typename: 'CiRunner',
|
||||
upgradeStatus: 'NOT_AVAILABLE',
|
||||
adminUrl: 'http://127.0.0.1:3000/admin/runners/1',
|
||||
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/2',
|
||||
description: 'test',
|
||||
runnerType: 'PROJECT_TYPE',
|
||||
shortSha: 'DdTYMQGA',
|
||||
version: '15.6.1',
|
||||
ipAddress: '127.0.0.1',
|
||||
active: true,
|
||||
locked: true,
|
||||
jobCount: 0,
|
||||
jobExecutionStatus: 'IDLE',
|
||||
tagList: ['tag3', 'tag4'],
|
||||
createdAt: '2022-11-29T09:37:43Z',
|
||||
contactedAt: null,
|
||||
status: 'NEVER_CONTACTED',
|
||||
userPermissions: {
|
||||
updateRunner: true,
|
||||
deleteRunner: true,
|
||||
__typename: 'RunnerPermissions',
|
||||
},
|
||||
groups: null,
|
||||
ownerProject: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
name: '123',
|
||||
nameWithNamespace: 'Administrator / 123',
|
||||
webUrl: 'http://127.0.0.1:3000/root/test',
|
||||
__typename: 'Project',
|
||||
},
|
||||
__typename: 'CiRunner',
|
||||
upgradeStatus: 'NOT_AVAILABLE',
|
||||
adminUrl: 'http://127.0.0.1:3000/admin/runners/2',
|
||||
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
__typename: 'CiRunnerConnection',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockCommitCreateResponseNewEtag = {
|
||||
data: {
|
||||
commitCreate: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
|
||||
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
|
||||
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
|
||||
import createStore from '~/ci/pipeline_editor/store';
|
||||
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
|
||||
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
|
||||
import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
|
||||
|
|
@ -80,7 +81,9 @@ describe('Pipeline editor app component', () => {
|
|||
provide = {},
|
||||
stubs = {},
|
||||
} = {}) => {
|
||||
const store = createStore();
|
||||
wrapper = shallowMount(PipelineEditorApp, {
|
||||
store,
|
||||
provide: { ...defaultProvide, ...provide },
|
||||
stubs,
|
||||
mocks: {
|
||||
|
|
@ -256,6 +259,10 @@ describe('Pipeline editor app component', () => {
|
|||
.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('available stages is updated', () => {
|
||||
expect(wrapper.vm.$store.state.availableStages).toStrictEqual(['test', 'build']);
|
||||
});
|
||||
|
||||
it('shows pipeline editor home component', () => {
|
||||
expect(findEditorHome().exists()).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,6 +70,34 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
|
|||
}
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
let!(:reporter) { create(:user, username: 'reporter') }
|
||||
let!(:report1) { create(:abuse_report) }
|
||||
let!(:report2) { create(:abuse_report, :closed, reporter: reporter, category: 'phishing') }
|
||||
|
||||
describe '.open' do
|
||||
subject(:results) { described_class.open }
|
||||
|
||||
it 'returns reports without resolved_at value' do
|
||||
expect(subject).to match_array([report, report1])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.closed' do
|
||||
subject(:results) { described_class.closed }
|
||||
|
||||
it 'returns reports with resolved_at value' do
|
||||
expect(subject).to match_array([report2])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_category' do
|
||||
it 'returns abuse reports with the specified category' do
|
||||
expect(described_class.by_category('phishing')).to match_array([report2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'before_validation' do
|
||||
context 'when links to spam contains empty strings' do
|
||||
let(:report) { create(:abuse_report, links_to_spam: ['', 'https://gitlab.com']) }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
|
|||
|
||||
expect(json_response).to be_a Hash
|
||||
expect(json_response['application_id']).to eq application.uid
|
||||
expect(json_response['secret']).to eq application.secret
|
||||
expect(application.secret_matches?(json_response['secret'])).to eq(true)
|
||||
expect(json_response['callback_url']).to eq application.redirect_uri
|
||||
expect(json_response['confidential']).to eq application.confidential
|
||||
expect(application.scopes.to_s).to eq('api')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
|
||||
let_it_be(:abuse_report) { build_stubbed(:abuse_report) }
|
||||
|
||||
let(:entity) do
|
||||
described_class.new(abuse_report)
|
||||
end
|
||||
|
||||
describe '#as_json' do
|
||||
subject(:entity_hash) { entity.as_json }
|
||||
|
||||
it 'exposes correct attributes' do
|
||||
expect(entity_hash.keys).to include(
|
||||
:category,
|
||||
:updated_at,
|
||||
:reported_user,
|
||||
:reporter
|
||||
)
|
||||
end
|
||||
|
||||
it 'correctly exposes `reported user`' do
|
||||
expect(entity_hash[:reported_user].keys).to match_array([:name])
|
||||
end
|
||||
|
||||
it 'correctly exposes `reporter`' do
|
||||
expect(entity_hash[:reporter].keys).to match_array([:name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Admin::AbuseReportSerializer, feature_category: :insider_threat do
|
||||
let(:resource) { build(:abuse_report) }
|
||||
|
||||
subject { described_class.new.represent(resource) }
|
||||
|
||||
describe '#represent' do
|
||||
it 'serializes an abuse report' do
|
||||
expect(subject[:id]).to eq resource.id
|
||||
end
|
||||
|
||||
context 'when multiple objects are being serialized' do
|
||||
let(:resource) { build_list(:abuse_report, 2) }
|
||||
|
||||
it 'serializers the array of abuse reports' do
|
||||
expect(subject).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -11,7 +11,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
DummyService.class_eval do
|
||||
include ::Issues::ResolveDiscussions
|
||||
|
||||
def initialize(project:, current_user: nil, params: {})
|
||||
def initialize(container:, current_user: nil, params: {})
|
||||
super
|
||||
filter_resolve_discussion_params
|
||||
end
|
||||
|
|
@ -26,7 +26,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") }
|
||||
|
||||
describe "#merge_request_for_resolving_discussion" do
|
||||
let(:service) { DummyService.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
|
||||
let(:service) { DummyService.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
|
||||
|
||||
it "finds the merge request" do
|
||||
expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
|
||||
|
|
@ -45,7 +45,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
describe "#discussions_to_resolve" do
|
||||
it "contains a single discussion when matching merge request and discussion are passed" do
|
||||
service = DummyService.new(
|
||||
project: project,
|
||||
container: project,
|
||||
current_user: user,
|
||||
params: {
|
||||
discussion_to_resolve: discussion.id,
|
||||
|
|
@ -65,7 +65,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
project: merge_request.target_project,
|
||||
line_number: 15)])
|
||||
service = DummyService.new(
|
||||
project: project,
|
||||
container: project,
|
||||
current_user: user,
|
||||
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
|
||||
)
|
||||
|
|
@ -83,7 +83,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
line_number: 15
|
||||
)])
|
||||
service = DummyService.new(
|
||||
project: project,
|
||||
container: project,
|
||||
current_user: user,
|
||||
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
|
||||
)
|
||||
|
|
@ -96,7 +96,7 @@ RSpec.describe Issues::ResolveDiscussions do
|
|||
|
||||
it "is empty when a discussion and another merge request are passed" do
|
||||
service = DummyService.new(
|
||||
project: project,
|
||||
container: project,
|
||||
current_user: user,
|
||||
params: {
|
||||
discussion_to_resolve: discussion.id,
|
||||
|
|
|
|||
Loading…
Reference in New Issue