diff options
Diffstat (limited to 'app')
14 files changed, 315 insertions, 153 deletions
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue index ecb39f214ec..8ee7132bb25 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; import { __, sprintf } from '~/locale'; +import { convertEnvironmentScope } from '../utils'; export default { name: 'CiEnvironmentsDropdown', @@ -12,7 +12,11 @@ export default { GlSearchBoxByType, }, props: { - value: { + environments: { + type: Array, + required: true, + }, + selectedEnvironmentScope: { type: String, required: false, default: '', @@ -24,31 +28,36 @@ export default { }; }, computed: { - ...mapGetters(['joinedEnvironments']), composedCreateButtonLabel() { return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm }); }, - shouldRenderCreateButton() { - return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm); - }, - filteredResults() { + filteredEnvironments() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedEnvironments.filter((resultString) => + return this.environments.filter((resultString) => resultString.toLowerCase().includes(lowerCasedSearchTerm), ); }, + shouldRenderCreateButton() { + return this.searchTerm && !this.environments.includes(this.searchTerm); + }, + environmentScopeLabel() { + return convertEnvironmentScope(this.selectedEnvironmentScope); + }, }, methods: { selectEnvironment(selected) { - this.$emit('selectEnvironment', selected); - this.searchTerm = ''; + this.$emit('select-environment', selected); + this.clearSearch(); }, - createClicked() { - this.$emit('createClicked', this.searchTerm); - this.searchTerm = ''; + convertEnvironmentScopeValue(scope) { + return convertEnvironmentScope(scope); + }, + createEnvironmentScope() { + this.$emit('create-environment-scope', this.searchTerm); + this.selectEnvironment(this.searchTerm); }, isSelected(env) { - return this.value === env; + return this.selectedEnvironmentScope === env; }, clearSearch() { this.searchTerm = ''; @@ -57,23 +66,23 @@ export default { }; </script> <template> - <gl-dropdown :text="value" @show="clearSearch"> + <gl-dropdown :text="environmentScopeLabel" @show="clearSearch"> <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" /> <gl-dropdown-item - v-for="environment in filteredResults" + v-for="environment in filteredEnvironments" :key="environment" :is-checked="isSelected(environment)" is-check-item @click="selectEnvironment(environment)" > - {{ environment }} + {{ convertEnvironmentScopeValue(environment) }} </gl-dropdown-item> - <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{ + <gl-dropdown-item v-if="!filteredEnvironments.length" ref="noMatchingResults">{{ __('No matching results') }}</gl-dropdown-item> <template v-if="shouldRenderCreateButton"> <gl-dropdown-divider /> - <gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked"> + <gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope"> {{ composedCreateButtonLabel }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index 557a8d6b5ba..dc57f3fe4ce 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -14,22 +14,26 @@ import { GlModal, GlSprintf, } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; import { getCookie, setCookie } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import Tracking from '~/tracking'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { mapComputed } from '~/vuex_shared/bindings'; + import { + allEnvironments, AWS_TOKEN_CONSTANTS, ADD_CI_VARIABLE_MODAL_ID, AWS_TIP_DISMISSED_COOKIE_NAME, AWS_TIP_MESSAGE, CONTAINS_VARIABLE_REFERENCE_MESSAGE, + defaultVariableState, ENVIRONMENT_SCOPE_LINK_TITLE, EVENT_LABEL, EVENT_ACTION, + EDIT_VARIABLE_ACTION, + VARIABLE_ACTIONS, + variableOptions, } from '../constants'; + import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; @@ -58,66 +62,75 @@ export default { GlModal, GlSprintf, }, - mixins: [glFeatureFlagsMixin(), trackingMixin], + mixins: [trackingMixin], + inject: [ + 'awsLogoSvgPath', + 'awsTipCommandsLink', + 'awsTipDeployLink', + 'awsTipLearnLink', + 'containsVariableReferenceLink', + 'environmentScopeLink', + 'isProtectedByDefault', + 'maskedEnvironmentVariablesLink', + 'maskableRegex', + 'protectedEnvironmentVariablesLink', + ], + props: { + areScopedVariablesAvailable: { + type: Boolean, + required: false, + default: false, + }, + environments: { + type: Array, + required: false, + default: () => [], + }, + mode: { + type: String, + required: true, + validator(val) { + return VARIABLE_ACTIONS.includes(val); + }, + }, + selectedVariable: { + type: Object, + required: false, + default: () => {}, + }, + }, data() { return { isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', + typeOptions: variableOptions, validationErrorEventProperty: '', + variable: { ...defaultVariableState, ...this.selectedVariable }, }; }, computed: { - ...mapState([ - 'projectId', - 'environments', - 'typeOptions', - 'variable', - 'variableBeingEdited', - 'isGroup', - 'maskableRegex', - 'selectedEnvironment', - 'isProtectedByDefault', - 'awsLogoSvgPath', - 'awsTipDeployLink', - 'awsTipCommandsLink', - 'awsTipLearnLink', - 'containsVariableReferenceLink', - 'protectedEnvironmentVariablesLink', - 'maskedEnvironmentVariablesLink', - 'environmentScopeLink', - ]), - ...mapComputed( - [ - { key: 'key', updateFn: 'updateVariableKey' }, - { key: 'secret_value', updateFn: 'updateVariableValue' }, - { key: 'variable_type', updateFn: 'updateVariableType' }, - { key: 'environment_scope', updateFn: 'setEnvironmentScope' }, - { key: 'protected_variable', updateFn: 'updateVariableProtected' }, - { key: 'masked', updateFn: 'updateVariableMasked' }, - ], - false, - 'variable', - ), - isTipVisible() { - return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); - }, - canSubmit() { - return ( - this.variableValidationState && - this.variable.key !== '' && - this.variable.secret_value !== '' - ); - }, canMask() { const regex = RegExp(this.maskableRegex); - return regex.test(this.variable.secret_value); + return regex.test(this.variable.value); + }, + canSubmit() { + return this.variableValidationState && this.variable.key !== '' && this.variable.value !== ''; }, containsVariableReference() { const regex = /\$/; - return regex.test(this.variable.secret_value); + return regex.test(this.variable.value); }, displayMaskedError() { return !this.canMask && this.variable.masked; }, + isEditing() { + return this.mode === EDIT_VARIABLE_ACTION; + }, + isTipVisible() { + return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); + }, + maskedFeedback() { + return this.displayMaskedError ? __('This variable can not be masked.') : ''; + }, maskedState() { if (this.displayMaskedError) { return false; @@ -125,10 +138,7 @@ export default { return true; }, modalActionText() { - return this.variableBeingEdited ? __('Update variable') : __('Add variable'); - }, - maskedFeedback() { - return this.displayMaskedError ? __('This variable can not be masked.') : ''; + return this.isEditing ? __('Update variable') : __('Add variable'); }, tokenValidationFeedback() { const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage; @@ -141,19 +151,16 @@ export default { const validator = this.$options.tokens?.[this.variable.key]?.validation; if (validator) { - return validator(this.variable.secret_value); + return validator(this.variable.value); } return true; }, - scopedVariablesAvailable() { - return !this.isGroup || this.glFeatures.groupScopedCiVariables; - }, variableValidationFeedback() { return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; }, variableValidationState() { - return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState); + return this.variable.value === '' || (this.tokenValidationState && this.maskedState); }, }, watch: { @@ -165,19 +172,18 @@ export default { }, }, methods: { - ...mapActions([ - 'addVariable', - 'updateVariable', - 'resetEditing', - 'displayInputValue', - 'clearModal', - 'deleteVariable', - 'setEnvironmentScope', - 'addWildCardScope', - 'resetSelectedEnvironment', - 'setSelectedEnvironment', - 'setVariableProtected', - ]), + addVariable() { + this.$emit('add-variable', this.variable); + }, + createEnvironmentScope(env) { + this.$emit('create-environment-scope', env); + }, + deleteVariable() { + this.$emit('delete-variable', this.variable); + }, + updateVariable() { + this.$emit('update-variable', this.variable); + }, dismissTip() { setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 }); this.isTipDismissed = true; @@ -190,16 +196,22 @@ export default { this.$refs.modal.hide(); }, resetModalHandler() { - if (this.variableBeingEdited) { - this.resetEditing(); - } - - this.clearModal(); - this.resetSelectedEnvironment(); + this.resetVariableData(); this.resetValidationErrorEvents(); + + this.$emit('hideModal'); + }, + resetVariableData() { + this.variable = { ...defaultVariableState }; + }, + setEnvironmentScope(scope) { + this.variable = { ...this.variable, environmentScope: scope }; + }, + setVariableProtected() { + this.variable = { ...this.variable, protected: true }; }, updateOrAddVariable() { - if (this.variableBeingEdited) { + if (this.isEditing) { this.updateVariable(); } else { this.addVariable(); @@ -207,7 +219,7 @@ export default { this.hideModal(); }, setVariableProtectedByDefault() { - if (this.isProtectedByDefault && !this.variableBeingEdited) { + if (this.isProtectedByDefault && !this.isEditing) { this.setVariableProtected(); } }, @@ -220,11 +232,11 @@ export default { }, getTrackingErrorProperty() { let property; - if (this.variable.secret_value?.length && !property) { + if (this.variable.value?.length && !property) { if (this.displayMaskedError && this.maskableRegex?.length) { const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, ''); const regex = new RegExp(supportedChars, 'g'); - property = this.variable.secret_value.replace(regex, ''); + property = this.variable.value.replace(regex, ''); } if (this.containsVariableReference) { property = '$'; @@ -237,6 +249,7 @@ export default { this.validationErrorEventProperty = ''; }, }, + defaultScope: allEnvironments.text, }; </script> @@ -252,7 +265,7 @@ export default { > <form> <gl-form-combobox - v-model="key" + v-model="variable.key" :token-list="$options.tokenList" :label-text="__('Key')" data-qa-selector="ci_variable_key_field" @@ -267,7 +280,7 @@ export default { <gl-form-textarea id="ci-variable-value" ref="valueField" - v-model="secret_value" + v-model="variable.value" :state="variableValidationState" rows="3" max-rows="6" @@ -278,7 +291,11 @@ export default { <div class="d-flex"> <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5"> - <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" /> + <gl-form-select + id="ci-variable-type" + v-model="variable.variableType" + :options="typeOptions" + /> </gl-form-group> <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope"> @@ -294,22 +311,24 @@ export default { </gl-link> </template> <ci-environments-dropdown - v-if="scopedVariablesAvailable" - class="w-100" - :value="environment_scope" - @selectEnvironment="setEnvironmentScope" - @createClicked="addWildCardScope" + v-if="areScopedVariablesAvailable" + class="gl-w-full" + :selected-environment-scope="variable.environmentScope" + :environments="environments" + @select-environment="setEnvironmentScope" + @create-environment-scope="createEnvironmentScope" /> - <gl-form-input v-else v-model="environment_scope" class="w-100" readonly /> + <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly /> </gl-form-group> </div> <gl-form-group :label="__('Flags')" label-for="ci-variable-flags"> <gl-form-checkbox - v-model="protected_variable" - class="mb-0" + v-model="variable.protected" + class="gl-mb-0" data-testid="ci-variable-protected-checkbox" + :data-is-protected-checked="variable.protected" > {{ __('Protect variable') }} <gl-link target="_blank" :href="protectedEnvironmentVariablesLink"> @@ -322,7 +341,7 @@ export default { <gl-form-checkbox ref="masked-ci-variable" - v-model="masked" + v-model="variable.masked" data-testid="ci-variable-masked-checkbox" > {{ __('Mask variable') }} @@ -403,7 +422,7 @@ export default { <template #modal-footer> <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button> <gl-button - v-if="variableBeingEdited" + v-if="isEditing" ref="deleteCiVariable" variant="danger" category="secondary" diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue index 7dcc5ce42d7..cebb7eb85ac 100644 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue @@ -30,7 +30,7 @@ import { EVENT_LABEL, EVENT_ACTION, } from '../constants'; -import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; +import LegacyCiEnvironmentsDropdown from './legacy_ci_environments_dropdown.vue'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; const trackingMixin = Tracking.mixin({ label: EVENT_LABEL }); @@ -43,7 +43,7 @@ export default { containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE, environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE, components: { - CiEnvironmentsDropdown, + LegacyCiEnvironmentsDropdown, GlAlert, GlButton, GlCollapse, @@ -293,7 +293,7 @@ export default { <gl-icon name="question" :size="12" /> </gl-link> </template> - <ci-environments-dropdown + <legacy-ci-environments-dropdown v-if="scopedVariablesAvailable" class="w-100" :value="environment_scope" diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js index fa55b4d9e77..e42a728a44e 100644 --- a/app/assets/javascripts/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci_variable_list/constants.js @@ -2,16 +2,49 @@ import { __ } from '~/locale'; export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; +// This const will be deprecated once we remove VueX from the section export const displayText = { variableText: __('Variable'), fileText: __('File'), allEnvironmentsText: __('All (default)'), }; +export const variableTypes = { + variableType: 'ENV_VAR', + fileType: 'FILE', +}; + +// Once REST is removed, we won't need `types` export const types = { variableType: 'env_var', fileType: 'file', - allEnvironmentsType: '*', +}; + +export const allEnvironments = { + type: '*', + text: __('All (default)'), +}; + +// Once REST is removed, we won't need `types` key +export const variableText = { + [types.variableType]: __('Variable'), + [types.fileType]: __('File'), + [variableTypes.variableType]: __('Variable'), + [variableTypes.fileType]: __('File'), +}; + +export const variableOptions = [ + { value: types.variableType, text: variableText[types.variableType] }, + { value: types.fileType, text: variableText[types.fileType] }, +]; + +export const defaultVariableState = { + environmentScope: allEnvironments.type, + key: '', + masked: false, + protected: false, + value: '', + variableType: types.variableType, }; export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed'; @@ -33,3 +66,20 @@ export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __( ); export const ENVIRONMENT_SCOPE_LINK_TITLE = __('Learn more'); + +export const ADD_VARIABLE_ACTION = 'ADD_VARIABLE'; +export const EDIT_VARIABLE_ACTION = 'EDIT_VARIABLE'; +export const VARIABLE_ACTIONS = [ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION]; + +export const GRAPHQL_PROJECT_TYPE = 'Project'; +export const GRAPHQL_GROUP_TYPE = 'Group'; + +export const ADD_MUTATION_ACTION = 'add'; +export const UPDATE_MUTATION_ACTION = 'update'; +export const DELETE_MUTATION_ACTION = 'delete'; + +export const environmentFetchErrorText = __( + 'There was an error fetching the environments information.', +); +export const genericMutationErrorText = __('Something went wrong on our end. Please try again.'); +export const variableFetchErrorText = __('There was an error fetching the variables.'); diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js index d9ca460a8e1..f46a671ae7b 100644 --- a/app/assets/javascripts/ci_variable_list/store/utils.js +++ b/app/assets/javascripts/ci_variable_list/store/utils.js @@ -1,5 +1,5 @@ import { cloneDeep } from 'lodash'; -import { displayText, types } from '../constants'; +import { displayText, types, allEnvironments } from '../constants'; const variableTypeHandler = (type) => type === displayText.variableText ? types.variableType : types.fileType; @@ -15,7 +15,7 @@ export const prepareDataForDisplay = (variables) => { } variableCopy.secret_value = variableCopy.value; - if (variableCopy.environment_scope === types.allEnvironmentsType) { + if (variableCopy.environment_scope === allEnvironments.type) { variableCopy.environment_scope = displayText.allEnvironmentsText; } variableCopy.protected_variable = variableCopy.protected; @@ -31,7 +31,7 @@ export const prepareDataForApi = (variable, destroy = false) => { variableCopy.masked = variableCopy.masked.toString(); variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type); if (variableCopy.environment_scope === displayText.allEnvironmentsText) { - variableCopy.environment_scope = types.allEnvironmentsType; + variableCopy.environment_scope = allEnvironments.type; } if (destroy) { diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js new file mode 100644 index 00000000000..009e56469d1 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/utils.js @@ -0,0 +1,45 @@ +import { uniq } from 'lodash'; +import { allEnvironments } from './constants'; + +/** + * This function takes aa list of variable and environments + * and create a new Array that concatenate the environment list + * with the environment scopes find in the variable list. This is + * useful for variable settings so that we can render a list of all + * environment scopes available based on both the list of envs and what + * is found under each variable. + * @param {Array} variables + * @param {Array} environments + * @returns {Array} - Array of environments + */ + +export const createJoinedEnvironments = (variables = [], environments = []) => { + const scopesFromVariables = variables.map((variable) => variable.environmentScope); + return uniq(environments.concat(scopesFromVariables)).sort(); +}; + +/** + * This function job is to convert the * wildcard to text when applicable + * in the UI. It uses a constants to compare the incoming value to that + * of the * and then apply the corresponding label if applicable. If there + * is no scope, then we return the default value as well. + * @param {String} scope + * @returns {String} - Converted value if applicable + */ + +export const convertEnvironmentScope = (environmentScope = '') => { + if (environmentScope === allEnvironments.type || !environmentScope) { + return allEnvironments.text; + } + + return environmentScope; +}; + +/** + * Gives us an array of all the environments by name + * @param {Array} nodes + * @return {Array<String>} - Array of environments strings + */ +export const mapEnvironmentNames = (nodes = []) => { + return nodes.map((env) => env.name); +}; diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js index 9d8ee165df2..3e5396c5bd8 100644 --- a/app/assets/javascripts/labels/labels_select.js +++ b/app/assets/javascripts/labels/labels_select.js @@ -438,7 +438,7 @@ export default class LabelsSelect { [ '<% if (isScopedLabel(label) && enableScopedLabels) { %>', "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>", - '<br />', + '<br>', '<%= escapeStr(label.description) %>', '<% } else { %>', '<%= escapeStr(label.description) %>', diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index fe84660422b..424ea3b61c5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { debounce } from 'lodash'; import DEFAULT_PROJECT_TEMPLATES from 'any_else_ce/projects/default_project_templates'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import Tracking from '~/tracking'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants'; import { ENTER_KEY } from '../lib/utils/keys'; import axios from '../lib/utils/axios_utils'; @@ -109,8 +110,31 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { ); }; + const projectPathValueListener = () => { + // eslint-disable-next-line no-param-reassign + $projectPathInput.oldInputValue = $projectPathInput.value; + }; + + const projectPathTrackListener = () => { + if ($projectPathInput.oldInputValue === $projectPathInput.value) { + // no change made to the input + return; + } + + const trackEvent = 'user_input_path_slug'; + const trackCategory = undefined; // will be default set in event method + + Tracking.event(trackCategory, trackEvent, { + label: 'new_project_form', + }); + }; + $projectPathInput.removeEventListener('keyup', projectPathInputListener); $projectPathInput.addEventListener('keyup', projectPathInputListener); + $projectPathInput.removeEventListener('focus', projectPathValueListener); + $projectPathInput.addEventListener('focus', projectPathValueListener); + $projectPathInput.removeEventListener('blur', projectPathTrackListener); + $projectPathInput.addEventListener('blur', projectPathTrackListener); $projectPathInput.removeEventListener('change', projectPathInputListener); $projectPathInput.addEventListener('change', projectPathInputListener); diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 5a655c92e46..5843e13c7cd 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -7,10 +7,14 @@ class SearchController < ApplicationController include ProductAnalyticsTracking include SearchRateLimitable - RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete].freeze + RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze track_event :show, name: 'i_search_total', destinations: [:redis_hll, :snowplow] + def self.search_rate_limited_endpoints + %i[show count autocomplete] + end + around_action :allow_gitaly_ref_name_caching before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch @@ -19,7 +23,7 @@ class SearchController < ApplicationController search_term_present = params[:search].present? || params[:term].present? search_term_present && !params[:project_id].present? end - before_action :check_search_rate_limit!, only: [:show, :count, :autocomplete] + before_action :check_search_rate_limit!, only: search_rate_limited_endpoints rescue_from ActiveRecord::QueryCanceled, with: :render_timeout @@ -32,8 +36,6 @@ class SearchController < ApplicationController @project = search_service.project @group = search_service.group - return if params[:search].blank? - return unless search_term_valid? return if check_single_commit_result? @@ -53,7 +55,6 @@ class SearchController < ApplicationController @search_results = @search_service.search_results @search_objects = @search_service.search_objects @search_highlight = @search_service.search_highlight - @aggregations = @search_service.search_aggregations end increment_search_counters @@ -99,6 +100,8 @@ class SearchController < ApplicationController end def search_term_valid? + return false if params[:search].blank? + unless search_service.valid_query_length? flash[:alert] = t('errors.messages.search_chars_too_long', count: Gitlab::Search::Params::SEARCH_CHAR_LIMIT) return false @@ -151,7 +154,7 @@ class SearchController < ApplicationController payload[:metadata]['meta.search.filters.state'] = params[:state] payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results] payload[:metadata]['meta.search.project_ids'] = params[:project_ids] - payload[:metadata]['meta.search.language'] = params[:language] + payload[:metadata]['meta.search.filters.language'] = params[:language] payload[:metadata]['meta.search.type'] = @search_type if @search_type.present? payload[:metadata]['meta.search.level'] = @search_level if @search_level.present? payload[:metadata][:global_search_duration_s] = @global_search_duration_s if @global_search_duration_s.present? @@ -207,7 +210,7 @@ class SearchController < ApplicationController case action_name.to_sym when :count render json: {}, status: :request_timeout - when :autocomplete + when :autocomplete, :aggregations render json: [], status: :request_timeout else render status: :request_timeout diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 3c7d358bde9..98cd831d6f1 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,6 +1,7 @@ - visibility_level = selected_visibility_level(@project, params.dig(:project, :visibility_level)) - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) - hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false) +- include_description = local_assigns.fetch(:include_description, true) - track_label = local_assigns.fetch(:track_label, 'blank_project') .row{ id: project_name_id } @@ -44,10 +45,19 @@ - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') } = html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe } -.form-group - = f.label :description, class: 'label-bold' do - = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe } - = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control gl-form-input", rows: 3, maxlength: 250, data: { qa_selector: 'project_description', track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_description", track_value: "" } +- if include_description + .form-group + = f.label :description, class: 'label-bold' do + = s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe } + = f.text_area :description, + placeholder: s_('ProjectsNew|Description format'), + class: "form-control gl-form-input", + rows: 3, + maxlength: 250, + data: { qa_selector: 'project_description', + track_label: track_label, + track_action: "activate_form_input", + track_property: "project_description" } - unless Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers? || !Gitlab.com? .js-deployment-target-select diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 07c38d9845c..56581fe7b18 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -13,7 +13,7 @@ .row{ 'v-cloak': true } #blank-project-pane.tab-pane.active = gitlab_ui_form_for @project, html: { class: 'new_project gl-mt-3' } do |f| - = render 'new_project_fields', f: f, project_name_id: "blank-project-name" + = render 'new_project_fields', f: f, project_name_id: "blank-project-name", include_description: false #create-from-template-pane.tab-pane = render Pajamas::CardComponent.new(card_options: { class: 'gl-my-5' }) do |c| diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 0ab99c83fd0..dccf61c6ec5 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,10 +1,9 @@ - unless @project.pages_deployed? - .card.border-info - .card-header.bg-info.text-white + = render Pajamas::CardComponent.new(card_options: { class: 'gl-border-blue-500' }, header_options: { class: 'gl-bg-blue-500 gl-text-white' }) do |c| + - c.header do = s_('GitLabPages|Configure pages') - .card-body - %p.gl-mb-0 - - docs_link_start = "<a href='#{help_page_path('user/project/pages/index')}' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_docs_link'>".html_safe - - samples_link_start = "<a href='https://gitlab.com/pages' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_samples_link'>".html_safe - - link_end = '</a>'.html_safe - = s_('GitLabPages|Your Pages site is not configured yet. See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also take some inspiration from the %{samples_link_start}sample Pages projects%{link_end}.').html_safe % { docs_link_start: docs_link_start, samples_link_start: samples_link_start, link_end: link_end } + - c.body do + - docs_link_start = "<a href='#{help_page_path('user/project/pages/index')}' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_docs_link'>".html_safe + - samples_link_start = "<a href='https://gitlab.com/pages' target='_blank' rel='noopener noreferrer' data-track-action='click_link' data-track-label='pages_samples_link'>".html_safe + - link_end = '</a>'.html_safe + = s_('GitLabPages|Your Pages site is not configured yet. See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also take some inspiration from the %{samples_link_start}sample Pages projects%{link_end}.').html_safe % { docs_link_start: docs_link_start, samples_link_start: samples_link_start, link_end: link_end } diff --git a/app/views/projects/project_templates/_template.html.haml b/app/views/projects/project_templates/_template.html.haml index 5e4b1397dd3..d0fdd3a729a 100644 --- a/app/views/projects/project_templates/_template.html.haml +++ b/app/views/projects/project_templates/_template.html.haml @@ -10,7 +10,8 @@ .controls.d-flex.align-items-center %a.btn.gl-button.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_action: "click_button", track_value: "" } } = _("Preview") - %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name } + %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name, + 'data-testid': "use_template_#{template.name}" } %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_action: "click_button", track_value: "" } } %span{ data: { qa_selector: 'use_template_button' } } = _("Use template") diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb index 8155b910677..0ec0a1b58d2 100644 --- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb @@ -15,32 +15,34 @@ module Gitlab # client - An instance of Gitlab::GithubImport::Client. # project - An instance of Project. def import(client, project) - importer = ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter - return skip_to_next_stage(project, importer) if feature_disabled?(project) + importer = importer_class(project) + return skip_to_next_stage(project) if importer.nil? - start_importer(project, importer, client) + info(project.id, message: "starting importer", importer: importer.name) + waiter = importer.new(project, client).execute + move_to_next_stage(project, { waiter.key => waiter.jobs_remaining }) end private - def start_importer(project, importer, client) - info(project.id, message: "starting importer", importer: importer.name) - waiter = importer.new(project, client).execute - move_to_next_stage(project, waiter.key => waiter.jobs_remaining) + def importer_class(project) + if Feature.enabled?(:github_importer_single_endpoint_issue_events_import, project.group, type: :ops) + ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter + elsif Feature.enabled?(:github_importer_issue_events_import, project.group, type: :ops) + ::Gitlab::GithubImport::Importer::IssueEventsImporter + else + nil + end end - def skip_to_next_stage(project, importer) - info(project.id, message: "skipping importer", importer: importer.name) + def skip_to_next_stage(project) + info(project.id, message: "skipping importer", importer: "IssueEventsImporter") move_to_next_stage(project) end def move_to_next_stage(project, waiters = {}) AdvanceStageWorker.perform_async(project.id, waiters, :notes) end - - def feature_disabled?(project) - Feature.disabled?(:github_importer_issue_events_import, project.group, type: :ops) - end end end end |