diff options
Diffstat (limited to 'app/assets')
25 files changed, 612 insertions, 1326 deletions
diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue new file mode 100644 index 00000000000..70974f2e725 --- /dev/null +++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue @@ -0,0 +1,171 @@ +<script> +import { GlButton, GlFormInput, GlModal, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import { sprintf, s__, __ } from '~/locale'; + +export const i18n = { + deleteButtonText: s__('Branches|Delete merged branches'), + buttonTooltipText: s__("Branches|Delete all branches that are merged into '%{defaultBranch}'"), + modalTitle: s__('Branches|Delete all merged branches?'), + modalMessage: s__( + 'Branches|You are about to %{strongStart}delete all branches%{strongEnd} that were merged into %{codeStart}%{defaultBranch}%{codeEnd}.', + ), + notVisibleBranchesWarning: s__( + 'Branches|This may include merged branches that are not visible on the current screen.', + ), + protectedBranchWarning: s__( + "Branches|A branch won't be deleted if it is protected or associated with an open merge request.", + ), + permanentEffectWarning: s__( + 'Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}.', + ), + confirmationMessage: s__( + 'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.', + ), + cancelButtonText: __('Cancel'), +}; + +export default { + csrf, + components: { + GlModal, + GlButton, + GlFormInput, + GlSprintf, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + formPath: { + type: String, + required: true, + }, + defaultBranch: { + type: String, + required: true, + }, + }, + data() { + return { + areAllBranchesVisible: false, + enteredText: '', + }; + }, + computed: { + buttonTooltipText() { + return sprintf(this.$options.i18n.buttonTooltipText, { defaultBranch: this.defaultBranch }); + }, + modalMessage() { + return sprintf(this.$options.i18n.modalMessage, { + defaultBranch: this.defaultBranch, + }); + }, + isDeletingConfirmed() { + return this.enteredText.trim().toLowerCase() === 'delete'; + }, + isDeleteButtonDisabled() { + return !this.isDeletingConfirmed; + }, + }, + methods: { + openModal() { + this.$refs.modal.show(); + }, + submitForm() { + if (!this.isDeleteButtonDisabled) { + this.$refs.form.submit(); + } + }, + closeModal() { + this.$refs.modal.hide(); + }, + }, + i18n, +}; +</script> + +<template> + <div> + <gl-button + v-gl-tooltip="buttonTooltipText" + class="gl-mr-3" + data-qa-selector="delete_merged_branches_button" + category="secondary" + variant="danger" + @click="openModal" + >{{ $options.i18n.deleteButtonText }} + </gl-button> + <gl-modal + ref="modal" + size="sm" + modal-id="delete-merged-branches" + :title="$options.i18n.modalTitle" + > + <form ref="form" :action="formPath" method="post" @submit.prevent> + <p> + <gl-sprintf :message="modalMessage"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </p> + <p> + {{ $options.i18n.notVisibleBranchesWarning }} + </p> + <p> + {{ $options.i18n.protectedBranchWarning }} + </p> + <p> + <gl-sprintf :message="$options.i18n.permanentEffectWarning"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </p> + <p> + <gl-sprintf :message="$options.i18n.confirmationMessage"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + <gl-form-input + v-model="enteredText" + data-qa-selector="delete_merged_branches_input" + type="text" + size="sm" + class="gl-mt-2" + aria-labelledby="input-label" + autocomplete="off" + @keyup.enter="submitForm" + /> + </p> + + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + </form> + + <template #modal-footer> + <div + class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3" + > + <gl-button data-testid="delete-merged-branches-cancel-button" @click="closeModal"> + {{ $options.i18n.cancelButtonText }} + </gl-button> + <gl-button + ref="deleteMergedBrancesButton" + :disabled="isDeleteButtonDisabled" + variant="danger" + data-qa-selector="delete_merged_branches_confirmation_button" + data-testid="delete-merged-branches-confirmation-button" + @click="submitForm" + >{{ $options.i18n.deleteButtonText }}</gl-button + > + </div> + </template> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/branches/init_delete_merged_branches.js b/app/assets/javascripts/branches/init_delete_merged_branches.js new file mode 100644 index 00000000000..998db07d8de --- /dev/null +++ b/app/assets/javascripts/branches/init_delete_merged_branches.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import DeleteMergedBranches from '~/branches/components/delete_merged_branches.vue'; + +export default function initDeleteMergedBranchesModal() { + const el = document.querySelector('.js-delete-merged-branches'); + if (!el) { + return false; + } + + const { formPath, defaultBranch } = el.dataset; + + return new Vue({ + el, + render(createComponent) { + return createComponent(DeleteMergedBranches, { + props: { + formPath, + defaultBranch, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue deleted file mode 100644 index ecb39f214ec..00000000000 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue +++ /dev/null @@ -1,81 +0,0 @@ -<script> -import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; -import { __, sprintf } from '~/locale'; - -export default { - name: 'CiEnvironmentsDropdown', - components: { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlSearchBoxByType, - }, - props: { - value: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - searchTerm: '', - }; - }, - computed: { - ...mapGetters(['joinedEnvironments']), - composedCreateButtonLabel() { - return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm }); - }, - shouldRenderCreateButton() { - return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm); - }, - filteredResults() { - const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedEnvironments.filter((resultString) => - resultString.toLowerCase().includes(lowerCasedSearchTerm), - ); - }, - }, - methods: { - selectEnvironment(selected) { - this.$emit('selectEnvironment', selected); - this.searchTerm = ''; - }, - createClicked() { - this.$emit('createClicked', this.searchTerm); - this.searchTerm = ''; - }, - isSelected(env) { - return this.value === env; - }, - clearSearch() { - this.searchTerm = ''; - }, - }, -}; -</script> -<template> - <gl-dropdown :text="value" @show="clearSearch"> - <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" /> - <gl-dropdown-item - v-for="environment in filteredResults" - :key="environment" - :is-checked="isSelected(environment)" - is-check-item - @click="selectEnvironment(environment)" - > - {{ environment }} - </gl-dropdown-item> - <gl-dropdown-item v-if="!filteredResults.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"> - {{ composedCreateButtonLabel }} - </gl-dropdown-item> - </template> - </gl-dropdown> -</template> 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 deleted file mode 100644 index fa90e0e3e6c..00000000000 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue +++ /dev/null @@ -1,429 +0,0 @@ -<script> -import { - GlAlert, - GlButton, - GlCollapse, - GlFormCheckbox, - GlFormCombobox, - GlFormGroup, - GlFormSelect, - GlFormInput, - GlFormTextarea, - GlIcon, - GlLink, - 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 { - AWS_TOKEN_CONSTANTS, - ADD_CI_VARIABLE_MODAL_ID, - AWS_TIP_DISMISSED_COOKIE_NAME, - AWS_TIP_MESSAGE, - CONTAINS_VARIABLE_REFERENCE_MESSAGE, - ENVIRONMENT_SCOPE_LINK_TITLE, - EVENT_LABEL, - EVENT_ACTION, -} from '../constants'; -import LegacyCiEnvironmentsDropdown from './legacy_ci_environments_dropdown.vue'; -import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; - -const trackingMixin = Tracking.mixin({ label: EVENT_LABEL }); - -export default { - modalId: ADD_CI_VARIABLE_MODAL_ID, - tokens: awsTokens, - tokenList: awsTokenList, - awsTipMessage: AWS_TIP_MESSAGE, - containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE, - environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE, - components: { - LegacyCiEnvironmentsDropdown, - GlAlert, - GlButton, - GlCollapse, - GlFormCheckbox, - GlFormCombobox, - GlFormGroup, - GlFormSelect, - GlFormInput, - GlFormTextarea, - GlIcon, - GlLink, - GlModal, - GlSprintf, - }, - mixins: [glFeatureFlagsMixin(), trackingMixin], - data() { - return { - isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', - validationErrorEventProperty: '', - }; - }, - 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); - }, - containsVariableReference() { - const regex = /\$/; - return regex.test(this.variable.secret_value); - }, - displayMaskedError() { - return !this.canMask && this.variable.masked; - }, - maskedState() { - if (this.displayMaskedError) { - return false; - } - return true; - }, - modalActionText() { - return this.variableBeingEdited ? __('Update variable') : __('Add variable'); - }, - maskedFeedback() { - return this.displayMaskedError ? __('This variable can not be masked.') : ''; - }, - tokenValidationFeedback() { - const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage; - if (!this.tokenValidationState && tokenSpecificFeedback) { - return tokenSpecificFeedback; - } - return ''; - }, - tokenValidationState() { - const validator = this.$options.tokens?.[this.variable.key]?.validation; - - if (validator) { - return validator(this.variable.secret_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); - }, - }, - watch: { - variable: { - handler() { - this.trackVariableValidationErrors(); - }, - deep: true, - }, - }, - methods: { - ...mapActions([ - 'addVariable', - 'updateVariable', - 'resetEditing', - 'displayInputValue', - 'clearModal', - 'deleteVariable', - 'setEnvironmentScope', - 'addWildCardScope', - 'resetSelectedEnvironment', - 'setSelectedEnvironment', - 'setVariableProtected', - ]), - dismissTip() { - setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 }); - this.isTipDismissed = true; - }, - deleteVarAndClose() { - this.deleteVariable(); - this.hideModal(); - }, - hideModal() { - this.$refs.modal.hide(); - }, - resetModalHandler() { - if (this.variableBeingEdited) { - this.resetEditing(); - } - - this.clearModal(); - this.resetSelectedEnvironment(); - this.resetValidationErrorEvents(); - }, - updateOrAddVariable() { - if (this.variableBeingEdited) { - this.updateVariable(); - } else { - this.addVariable(); - } - this.hideModal(); - }, - setVariableProtectedByDefault() { - if (this.isProtectedByDefault && !this.variableBeingEdited) { - this.setVariableProtected(); - } - }, - trackVariableValidationErrors() { - const property = this.getTrackingErrorProperty(); - if (!this.validationErrorEventProperty && property) { - this.track(EVENT_ACTION, { property }); - this.validationErrorEventProperty = property; - } - }, - getTrackingErrorProperty() { - let property; - if (this.variable.secret_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, ''); - } - if (this.containsVariableReference) { - property = '$'; - } - } - - return property; - }, - resetValidationErrorEvents() { - this.validationErrorEventProperty = ''; - }, - }, -}; -</script> - -<template> - <gl-modal - ref="modal" - :modal-id="$options.modalId" - :title="modalActionText" - static - lazy - @hidden="resetModalHandler" - @shown="setVariableProtectedByDefault" - > - <form> - <gl-form-combobox - v-model="key" - :token-list="$options.tokenList" - :label-text="__('Key')" - data-testid="pipeline-form-ci-variable-key" - data-qa-selector="ci_variable_key_field" - /> - - <gl-form-group - :label="__('Value')" - label-for="ci-variable-value" - :state="variableValidationState" - :invalid-feedback="variableValidationFeedback" - > - <gl-form-textarea - id="ci-variable-value" - ref="valueField" - v-model="secret_value" - :state="variableValidationState" - rows="3" - max-rows="10" - data-testid="pipeline-form-ci-variable-value" - data-qa-selector="ci_variable_value_field" - class="gl-font-monospace!" - spellcheck="false" - /> - </gl-form-group> - - <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-group> - - <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope"> - <template #label> - {{ __('Environment scope') }} - <gl-link - :title="$options.environmentScopeLinkTitle" - :href="environmentScopeLink" - target="_blank" - data-testid="environment-scope-link" - > - <gl-icon name="question" :size="12" /> - </gl-link> - </template> - <legacy-ci-environments-dropdown - v-if="scopedVariablesAvailable" - class="w-100" - :value="environment_scope" - @selectEnvironment="setEnvironmentScope" - @createClicked="addWildCardScope" - /> - - <gl-form-input v-else v-model="environment_scope" class="w-100" 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" - data-testid="ci-variable-protected-checkbox" - > - {{ __('Protect variable') }} - <gl-link target="_blank" :href="protectedEnvironmentVariablesLink"> - <gl-icon name="question" :size="12" /> - </gl-link> - <p class="gl-mt-2 text-secondary"> - {{ __('Export variable to pipelines running on protected branches and tags only.') }} - </p> - </gl-form-checkbox> - - <gl-form-checkbox - ref="masked-ci-variable" - v-model="masked" - data-testid="ci-variable-masked-checkbox" - > - {{ __('Mask variable') }} - <gl-link target="_blank" :href="maskedEnvironmentVariablesLink"> - <gl-icon name="question" :size="12" /> - </gl-link> - <p class="gl-mt-2 gl-mb-0 text-secondary"> - {{ __('Variable will be masked in job logs.') }} - <span - :class="{ - 'bold text-plain': displayMaskedError, - }" - > - {{ __('Requires values to meet regular expression requirements.') }}</span - > - <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{ - __('More information') - }}</gl-link> - </p> - </gl-form-checkbox> - </gl-form-group> - </form> - <gl-collapse :visible="isTipVisible"> - <gl-alert - :title="__('Deploying to AWS is easy with GitLab')" - variant="tip" - data-testid="aws-guidance-tip" - @dismiss="dismissTip" - > - <div class="gl-display-flex gl-flex-direction-row"> - <div> - <p> - <gl-sprintf :message="$options.awsTipMessage"> - <template #deployLink="{ content }"> - <gl-link :href="awsTipDeployLink" target="_blank">{{ content }}</gl-link> - </template> - <template #commandsLink="{ content }"> - <gl-link :href="awsTipCommandsLink" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </p> - <p> - <gl-button - :href="awsTipLearnLink" - target="_blank" - category="secondary" - variant="info" - class="gl-overflow-wrap-break" - >{{ __('Learn more about deploying to AWS') }}</gl-button - > - </p> - </div> - <img - class="gl-mt-3" - :alt="__('Amazon Web Services Logo')" - :src="awsLogoSvgPath" - height="32" - /> - </div> - </gl-alert> - </gl-collapse> - <gl-alert - v-if="containsVariableReference" - :title="__('Value might contain a variable reference')" - :dismissible="false" - variant="warning" - data-testid="contains-variable-reference" - > - <gl-sprintf :message="$options.containsVariableReferenceMessage"> - <template #code="{ content }"> - <code>{{ content }}</code> - </template> - <template #docsLink="{ content }"> - <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </gl-alert> - <template #modal-footer> - <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button> - <gl-button - v-if="variableBeingEdited" - ref="deleteCiVariable" - variant="danger" - category="secondary" - data-qa-selector="ci_variable_delete_button" - @click="deleteVarAndClose" - >{{ __('Delete variable') }}</gl-button - > - <gl-button - ref="updateOrAddVariable" - :disabled="!canSubmit" - variant="confirm" - category="primary" - data-testid="ciUpdateOrAddVariableBtn" - data-qa-selector="ci_variable_save_button" - @click="updateOrAddVariable" - >{{ modalActionText }} - </gl-button> - </template> - </gl-modal> -</template> diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue deleted file mode 100644 index f1fe188348d..00000000000 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script> -import { mapState, mapActions } from 'vuex'; -import LegacyCiVariableModal from './legacy_ci_variable_modal.vue'; -import LegacyCiVariableTable from './legacy_ci_variable_table.vue'; - -export default { - components: { - LegacyCiVariableModal, - LegacyCiVariableTable, - }, - computed: { - ...mapState(['isGroup', 'isProject']), - }, - mounted() { - if (this.isProject) { - this.fetchEnvironments(); - } - }, - methods: { - ...mapActions(['fetchEnvironments']), - }, -}; -</script> - -<template> - <div class="row"> - <div class="col-lg-12"> - <legacy-ci-variable-table /> - <legacy-ci-variable-modal /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue deleted file mode 100644 index f3a84e22316..00000000000 --- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue +++ /dev/null @@ -1,209 +0,0 @@ -<script> -import { GlTable, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import { s__, __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { ADD_CI_VARIABLE_MODAL_ID } from '../constants'; - -export default { - modalId: ADD_CI_VARIABLE_MODAL_ID, - fields: [ - { - key: 'variable_type', - label: s__('CiVariables|Type'), - thClass: 'gl-w-10p', - }, - { - key: 'key', - label: s__('CiVariables|Key'), - tdClass: 'text-plain', - sortable: true, - }, - { - key: 'value', - label: s__('CiVariables|Value'), - thClass: 'gl-w-15p', - }, - { - key: 'options', - label: s__('CiVariables|Options'), - thClass: 'gl-w-10p', - }, - { - key: 'environment_scope', - label: s__('CiVariables|Environments'), - }, - { - key: 'actions', - label: '', - tdClass: 'text-right', - thClass: 'gl-w-5p', - }, - ], - components: { - GlButton, - GlTable, - }, - directives: { - GlModalDirective, - GlTooltip: GlTooltipDirective, - }, - mixins: [glFeatureFlagsMixin()], - computed: { - ...mapState(['variables', 'valuesHidden', 'isLoading', 'isDeleting']), - valuesButtonText() { - return this.valuesHidden ? __('Reveal values') : __('Hide values'); - }, - isTableEmpty() { - return !this.variables || this.variables.length === 0; - }, - fields() { - return this.$options.fields; - }, - variablesWithOptions() { - return this.variables?.map((item, index) => ({ - ...item, - options: this.getOptions(item), - index, - })); - }, - }, - mounted() { - this.fetchVariables(); - }, - methods: { - ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']), - getOptions(item) { - const options = []; - if (item.protected) { - options.push(s__('CiVariables|Protected')); - } - if (item.masked) { - options.push(s__('CiVariables|Masked')); - } - return options.join(', '); - }, - editVariableClicked(index = -1) { - this.editVariable(this.variables[index] ?? null); - }, - }, -}; -</script> - -<template> - <div class="ci-variable-table" data-testid="ci-variable-table"> - <gl-table - :fields="fields" - :items="variablesWithOptions" - tbody-tr-class="js-ci-variable-row" - data-qa-selector="ci_variable_table_content" - sort-by="key" - sort-direction="asc" - stacked="lg" - table-class="text-secondary" - fixed - show-empty - sort-icon-left - no-sort-reset - > - <template #table-colgroup="scope"> - <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" /> - </template> - <template #cell(key)="{ item }"> - <div - class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" - > - <span - :id="`ci-variable-key-${item.id}`" - class="gl-display-inline-block gl-max-w-full gl-word-break-word" - >{{ item.key }}</span - > - <gl-button - v-gl-tooltip - category="tertiary" - icon="copy-to-clipboard" - class="gl-my-n3 gl-ml-2" - :title="__('Copy key')" - :data-clipboard-text="item.key" - :aria-label="__('Copy to clipboard')" - /> - </div> - </template> - <template #cell(value)="{ item }"> - <div - class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" - > - <span v-if="valuesHidden">*****</span> - <span - v-else - :id="`ci-variable-value-${item.id}`" - class="gl-display-inline-block gl-max-w-full gl-text-truncate" - >{{ item.value }}</span - > - <gl-button - v-gl-tooltip - category="tertiary" - icon="copy-to-clipboard" - class="gl-my-n3 gl-ml-2" - :title="__('Copy value')" - :data-clipboard-text="item.value" - :aria-label="__('Copy to clipboard')" - /> - </div> - </template> - <template #cell(options)="{ item }"> - <span>{{ item.options }}</span> - </template> - <template #cell(environment_scope)="{ item }"> - <div - class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" - > - <span - :id="`ci-variable-env-${item.id}`" - class="gl-display-inline-block gl-max-w-full gl-word-break-word" - >{{ item.environment_scope }}</span - > - <gl-button - v-gl-tooltip - category="tertiary" - icon="copy-to-clipboard" - class="gl-my-n3 gl-ml-2" - :title="__('Copy environment')" - :data-clipboard-text="item.environment_scope" - :aria-label="__('Copy to clipboard')" - /> - </div> - </template> - <template #cell(actions)="{ item }"> - <gl-button - v-gl-modal-directive="$options.modalId" - icon="pencil" - :aria-label="__('Edit')" - data-qa-selector="edit_ci_variable_button" - @click="editVariableClicked(item.index)" - /> - </template> - <template #empty> - <p class="gl-text-center gl-py-6 gl-text-black-normal gl-mb-0"> - {{ __('There are no variables yet.') }} - </p> - </template> - </gl-table> - <div class="ci-variable-actions gl-display-flex gl-mt-5"> - <gl-button - v-gl-modal-directive="$options.modalId" - class="gl-mr-3" - data-qa-selector="add_ci_variable_button" - variant="confirm" - category="primary" - >{{ __('Add variable') }}</gl-button - > - <gl-button - v-if="!isTableEmpty" - data-qa-selector="reveal_ci_variable_value_button" - @click="toggleValues(!valuesHidden)" - >{{ valuesButtonText }}</gl-button - > - </div> - </div> -</template> diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js index 1b69da9e086..174a59aba42 100644 --- a/app/assets/javascripts/ci_variable_list/index.js +++ b/app/assets/javascripts/ci_variable_list/index.js @@ -5,9 +5,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import CiAdminVariables from './components/ci_admin_variables.vue'; import CiGroupVariables from './components/ci_group_variables.vue'; import CiProjectVariables from './components/ci_project_variables.vue'; -import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue'; import { cacheConfig, resolvers } from './graphql/settings'; -import createStore from './store'; const mountCiVariableListApp = (containerEl) => { const { @@ -76,62 +74,10 @@ const mountCiVariableListApp = (containerEl) => { }); }; -const mountLegacyCiVariableListApp = (containerEl) => { - const { - endpoint, - projectId, - isGroup, - isProject, - maskableRegex, - protectedByDefault, - awsLogoSvgPath, - awsTipDeployLink, - awsTipCommandsLink, - awsTipLearnLink, - containsVariableReferenceLink, - protectedEnvironmentVariablesLink, - maskedEnvironmentVariablesLink, - environmentScopeLink, - } = containerEl.dataset; - - const parsedIsProject = parseBoolean(isProject); - const parsedIsGroup = parseBoolean(isGroup); - const isProtectedByDefault = parseBoolean(protectedByDefault); - - const store = createStore({ - endpoint, - projectId, - isGroup: parsedIsGroup, - isProject: parsedIsProject, - maskableRegex, - isProtectedByDefault, - awsLogoSvgPath, - awsTipDeployLink, - awsTipCommandsLink, - awsTipLearnLink, - containsVariableReferenceLink, - protectedEnvironmentVariablesLink, - maskedEnvironmentVariablesLink, - environmentScopeLink, - }); - - return new Vue({ - el: containerEl, - store, - render(createElement) { - return createElement(LegacyCiVariableSettings); - }, - }); -}; - -export default (containerId = 'js-ci-project-variables') => { +export default (containerId = 'js-ci-variables') => { const el = document.getElementById(containerId); - if (el) { - if (gon.features?.ciVariableSettingsGraphql) { - mountCiVariableListApp(el); - } else { - mountLegacyCiVariableListApp(el); - } - } + if (!el) return; + + mountCiVariableListApp(el); }; diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js deleted file mode 100644 index ac31e845b0d..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/actions.js +++ /dev/null @@ -1,208 +0,0 @@ -import Api from '~/api'; -import { createAlert } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import * as types from './mutation_types'; -import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils'; - -export const toggleValues = ({ commit }, valueState) => { - commit(types.TOGGLE_VALUES, valueState); -}; - -export const clearModal = ({ commit }) => { - commit(types.CLEAR_MODAL); -}; - -export const resetEditing = ({ commit, dispatch }) => { - // fetch variables again if modal is being edited and then hidden - // without saving changes, to cover use case of reactivity in the table - dispatch('fetchVariables'); - commit(types.RESET_EDITING); -}; - -export const setVariableProtected = ({ commit }) => { - commit(types.SET_VARIABLE_PROTECTED); -}; - -export const requestAddVariable = ({ commit }) => { - commit(types.REQUEST_ADD_VARIABLE); -}; - -export const receiveAddVariableSuccess = ({ commit }) => { - commit(types.RECEIVE_ADD_VARIABLE_SUCCESS); -}; - -export const receiveAddVariableError = ({ commit }, error) => { - commit(types.RECEIVE_ADD_VARIABLE_ERROR, error); -}; - -export const addVariable = ({ state, dispatch }) => { - dispatch('requestAddVariable'); - - return axios - .patch(state.endpoint, { - variables_attributes: [prepareDataForApi(state.variable)], - }) - .then(() => { - dispatch('receiveAddVariableSuccess'); - dispatch('fetchVariables'); - }) - .catch((error) => { - createAlert({ - message: error.response.data[0], - }); - dispatch('receiveAddVariableError', error); - }); -}; - -export const requestUpdateVariable = ({ commit }) => { - commit(types.REQUEST_UPDATE_VARIABLE); -}; - -export const receiveUpdateVariableSuccess = ({ commit }) => { - commit(types.RECEIVE_UPDATE_VARIABLE_SUCCESS); -}; - -export const receiveUpdateVariableError = ({ commit }, error) => { - commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error); -}; - -export const updateVariable = ({ state, dispatch }) => { - dispatch('requestUpdateVariable'); - - const updatedVariable = prepareDataForApi(state.variable); - updatedVariable.secrect_value = updateVariable.value; - - return axios - .patch(state.endpoint, { variables_attributes: [updatedVariable] }) - .then(() => { - dispatch('receiveUpdateVariableSuccess'); - dispatch('fetchVariables'); - }) - .catch((error) => { - createAlert({ - message: error.response.data[0], - }); - dispatch('receiveUpdateVariableError', error); - }); -}; - -export const editVariable = ({ commit }, variable) => { - const variableToEdit = variable; - variableToEdit.secret_value = variableToEdit.value; - commit(types.VARIABLE_BEING_EDITED, variableToEdit); -}; - -export const requestVariables = ({ commit }) => { - commit(types.REQUEST_VARIABLES); -}; -export const receiveVariablesSuccess = ({ commit }, variables) => { - commit(types.RECEIVE_VARIABLES_SUCCESS, variables); -}; - -export const fetchVariables = ({ dispatch, state }) => { - dispatch('requestVariables'); - - return axios - .get(state.endpoint) - .then(({ data }) => { - dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables)); - }) - .catch(() => { - createAlert({ - message: __('There was an error fetching the variables.'), - }); - }); -}; - -export const requestDeleteVariable = ({ commit }) => { - commit(types.REQUEST_DELETE_VARIABLE); -}; - -export const receiveDeleteVariableSuccess = ({ commit }) => { - commit(types.RECEIVE_DELETE_VARIABLE_SUCCESS); -}; - -export const receiveDeleteVariableError = ({ commit }, error) => { - commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error); -}; - -export const deleteVariable = ({ dispatch, state }) => { - dispatch('requestDeleteVariable'); - - const destroy = true; - - return axios - .patch(state.endpoint, { variables_attributes: [prepareDataForApi(state.variable, destroy)] }) - .then(() => { - dispatch('receiveDeleteVariableSuccess'); - dispatch('fetchVariables'); - }) - .catch((error) => { - createAlert({ - message: error.response.data[0], - }); - dispatch('receiveDeleteVariableError', error); - }); -}; - -export const requestEnvironments = ({ commit }) => { - commit(types.REQUEST_ENVIRONMENTS); -}; - -export const receiveEnvironmentsSuccess = ({ commit }, environments) => { - commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments); -}; - -export const fetchEnvironments = ({ dispatch, state }) => { - dispatch('requestEnvironments'); - - return Api.environments(state.projectId) - .then((res) => { - dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data)); - }) - .catch(() => { - createAlert({ - message: __('There was an error fetching the environments information.'), - }); - }); -}; - -export const setEnvironmentScope = ({ commit, dispatch }, environment) => { - commit(types.SET_ENVIRONMENT_SCOPE, environment); - dispatch('setSelectedEnvironment', environment); -}; - -export const addWildCardScope = ({ commit, dispatch }, environment) => { - commit(types.ADD_WILD_CARD_SCOPE, environment); - commit(types.SET_ENVIRONMENT_SCOPE, environment); - dispatch('setSelectedEnvironment', environment); -}; - -export const resetSelectedEnvironment = ({ commit }) => { - commit(types.RESET_SELECTED_ENVIRONMENT); -}; - -export const setSelectedEnvironment = ({ commit }, environment) => { - commit(types.SET_SELECTED_ENVIRONMENT, environment); -}; - -export const updateVariableKey = ({ commit }, { key }) => { - commit(types.UPDATE_VARIABLE_KEY, key); -}; - -export const updateVariableValue = ({ commit }, { secret_value }) => { - commit(types.UPDATE_VARIABLE_VALUE, secret_value); -}; - -export const updateVariableType = ({ commit }, { variable_type }) => { - commit(types.UPDATE_VARIABLE_TYPE, variable_type); -}; - -export const updateVariableProtected = ({ commit }, { protected_variable }) => { - commit(types.UPDATE_VARIABLE_PROTECTED, protected_variable); -}; - -export const updateVariableMasked = ({ commit }, { masked }) => { - commit(types.UPDATE_VARIABLE_MASKED, masked); -}; diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js deleted file mode 100644 index 6570f455541..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/getters.js +++ /dev/null @@ -1,6 +0,0 @@ -import { uniq } from 'lodash'; - -export const joinedEnvironments = (state) => { - const scopesFromVariables = (state.variables || []).map((variable) => variable.environment_scope); - return uniq(state.environments.concat(scopesFromVariables)).sort(); -}; diff --git a/app/assets/javascripts/ci_variable_list/store/index.js b/app/assets/javascripts/ci_variable_list/store/index.js deleted file mode 100644 index 83802f6a36f..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -Vue.use(Vuex); - -export default (initialState = {}) => - new Vuex.Store({ - actions, - mutations, - getters, - state: { - ...state(), - ...initialState, - }, - }); diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js deleted file mode 100644 index 5db8f610192..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/mutation_types.js +++ /dev/null @@ -1,33 +0,0 @@ -export const TOGGLE_VALUES = 'TOGGLE_VALUES'; -export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED'; -export const RESET_EDITING = 'RESET_EDITING'; -export const CLEAR_MODAL = 'CLEAR_MODAL'; -export const SET_VARIABLE_PROTECTED = 'SET_VARIABLE_PROTECTED'; - -export const REQUEST_VARIABLES = 'REQUEST_VARIABLES'; -export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS'; - -export const REQUEST_DELETE_VARIABLE = 'REQUEST_DELETE_VARIABLE'; -export const RECEIVE_DELETE_VARIABLE_SUCCESS = 'RECEIVE_DELETE_VARIABLE_SUCCESS'; -export const RECEIVE_DELETE_VARIABLE_ERROR = 'RECEIVE_DELETE_VARIABLE_ERROR'; - -export const REQUEST_ADD_VARIABLE = 'REQUEST_ADD_VARIABLE'; -export const RECEIVE_ADD_VARIABLE_SUCCESS = 'RECEIVE_ADD_VARIABLE_SUCCESS'; -export const RECEIVE_ADD_VARIABLE_ERROR = 'RECEIVE_ADD_VARIABLE_ERROR'; - -export const REQUEST_UPDATE_VARIABLE = 'REQUEST_UPDATE_VARIABLE'; -export const RECEIVE_UPDATE_VARIABLE_SUCCESS = 'RECEIVE_UPDATE_VARIABLE_SUCCESS'; -export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR'; - -export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS'; -export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS'; -export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE'; -export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE'; -export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT'; -export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT'; - -export const UPDATE_VARIABLE_KEY = 'UPDATE_VARIABLE_KEY'; -export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE'; -export const UPDATE_VARIABLE_TYPE = 'UPDATE_VARIABLE_TYPE'; -export const UPDATE_VARIABLE_PROTECTED = 'UPDATE_VARIABLE_PROTECTED'; -export const UPDATE_VARIABLE_MASKED = 'UPDATE_VARIABLE_MASKED'; diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js deleted file mode 100644 index 0e7c61cecb8..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/mutations.js +++ /dev/null @@ -1,128 +0,0 @@ -import { displayText } from '../constants'; -import * as types from './mutation_types'; - -export default { - [types.REQUEST_VARIABLES](state) { - state.isLoading = true; - }, - - [types.RECEIVE_VARIABLES_SUCCESS](state, variables) { - state.isLoading = false; - state.variables = variables; - }, - - [types.REQUEST_DELETE_VARIABLE](state) { - state.isDeleting = true; - }, - - [types.RECEIVE_DELETE_VARIABLE_SUCCESS](state) { - state.isDeleting = false; - }, - - [types.RECEIVE_DELETE_VARIABLE_ERROR](state, error) { - state.isDeleting = false; - state.error = error; - }, - - [types.REQUEST_ADD_VARIABLE](state) { - state.isLoading = true; - }, - - [types.RECEIVE_ADD_VARIABLE_SUCCESS](state) { - state.isLoading = false; - }, - - [types.RECEIVE_ADD_VARIABLE_ERROR](state, error) { - state.isLoading = false; - state.error = error; - }, - - [types.REQUEST_UPDATE_VARIABLE](state) { - state.isLoading = true; - }, - - [types.RECEIVE_UPDATE_VARIABLE_SUCCESS](state) { - state.isLoading = false; - }, - - [types.RECEIVE_UPDATE_VARIABLE_ERROR](state, error) { - state.isLoading = false; - state.error = error; - }, - - [types.TOGGLE_VALUES](state, valueState) { - state.valuesHidden = valueState; - }, - - [types.REQUEST_ENVIRONMENTS](state) { - state.isLoading = true; - }, - - [types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) { - state.isLoading = false; - state.environments = environments; - state.environments.unshift(displayText.allEnvironmentsText); - }, - - [types.VARIABLE_BEING_EDITED](state, variable) { - state.variableBeingEdited = true; - state.variable = variable; - }, - - [types.CLEAR_MODAL](state) { - state.variable = { - variable_type: displayText.variableText, - key: '', - secret_value: '', - protected_variable: false, - masked: false, - environment_scope: displayText.allEnvironmentsText, - }; - }, - - [types.RESET_EDITING](state) { - state.variableBeingEdited = false; - state.showInputValue = false; - }, - - [types.SET_ENVIRONMENT_SCOPE](state, environment) { - state.variable.environment_scope = environment; - }, - - [types.ADD_WILD_CARD_SCOPE](state, environment) { - state.environments.push(environment); - state.environments.sort(); - }, - - [types.RESET_SELECTED_ENVIRONMENT](state) { - state.selectedEnvironment = ''; - }, - - [types.SET_SELECTED_ENVIRONMENT](state, environment) { - state.selectedEnvironment = environment; - }, - - [types.SET_VARIABLE_PROTECTED](state) { - state.variable.protected_variable = true; - }, - - [types.UPDATE_VARIABLE_KEY](state, key) { - state.variable.key = key; - }, - - [types.UPDATE_VARIABLE_VALUE](state, value) { - state.variable.secret_value = value; - }, - - [types.UPDATE_VARIABLE_TYPE](state, type) { - state.variable.variable_type = type; - }, - - [types.UPDATE_VARIABLE_PROTECTED](state, bool) { - state.variable.protected_variable = bool; - }, - - [types.UPDATE_VARIABLE_MASKED](state, bool) { - state.variable.masked = bool; - }, -}; diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js deleted file mode 100644 index 96b27792664..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/state.js +++ /dev/null @@ -1,26 +0,0 @@ -import { displayText } from '../constants'; - -export default () => ({ - endpoint: null, - projectId: null, - isGroup: null, - maskableRegex: null, - isProtectedByDefault: null, - isLoading: false, - isDeleting: false, - variable: { - variable_type: displayText.variableText, - key: '', - secret_value: '', - protected_variable: false, - masked: false, - environment_scope: displayText.allEnvironmentsText, - }, - variables: null, - valuesHidden: true, - error: null, - environments: [], - typeOptions: [displayText.variableText, displayText.fileText], - variableBeingEdited: false, - selectedEnvironment: '', -}); diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js deleted file mode 100644 index f46a671ae7b..00000000000 --- a/app/assets/javascripts/ci_variable_list/store/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -import { cloneDeep } from 'lodash'; -import { displayText, types, allEnvironments } from '../constants'; - -const variableTypeHandler = (type) => - type === displayText.variableText ? types.variableType : types.fileType; - -export const prepareDataForDisplay = (variables) => { - const variablesToDisplay = []; - variables.forEach((variable) => { - const variableCopy = variable; - if (variableCopy.variable_type === types.variableType) { - variableCopy.variable_type = displayText.variableText; - } else { - variableCopy.variable_type = displayText.fileText; - } - variableCopy.secret_value = variableCopy.value; - - if (variableCopy.environment_scope === allEnvironments.type) { - variableCopy.environment_scope = displayText.allEnvironmentsText; - } - variableCopy.protected_variable = variableCopy.protected; - variablesToDisplay.push(variableCopy); - }); - return variablesToDisplay; -}; - -export const prepareDataForApi = (variable, destroy = false) => { - const variableCopy = cloneDeep(variable); - variableCopy.protected = variableCopy.protected_variable.toString(); - delete variableCopy.protected_variable; - variableCopy.masked = variableCopy.masked.toString(); - variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type); - if (variableCopy.environment_scope === displayText.allEnvironmentsText) { - variableCopy.environment_scope = allEnvironments.type; - } - - if (destroy) { - // eslint-disable-next-line - variableCopy._destroy = destroy; - } - - return variableCopy; -}; - -export const prepareEnvironments = (environments) => environments.map((e) => e.name); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue new file mode 100644 index 00000000000..c7fddadab1b --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue @@ -0,0 +1,91 @@ +<script> +import { GlFormCheckbox, GlFormGroup, GlSprintf } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { + PACKAGE_FORWARDING_CHECKBOX_LABEL, + PACKAGE_FORWARDING_ENFORCE_LABEL, +} from '~/packages_and_registries/settings/group/constants'; + +export default { + name: 'ForwardingSettings', + i18n: { + PACKAGE_FORWARDING_CHECKBOX_LABEL, + PACKAGE_FORWARDING_ENFORCE_LABEL, + }, + components: { + GlFormCheckbox, + GlFormGroup, + GlSprintf, + }, + props: { + disabled: { + type: Boolean, + required: false, + default: true, + }, + forwarding: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: true, + }, + lockForwarding: { + type: Boolean, + required: false, + default: false, + }, + modelNames: { + type: Object, + required: true, + validator(value) { + return isEqual(Object.keys(value), ['forwarding', 'lockForwarding', 'isLocked']); + }, + }, + }, + computed: { + fields() { + return [ + { + testid: 'forwarding-checkbox', + label: PACKAGE_FORWARDING_CHECKBOX_LABEL, + updateField: this.modelNames.forwarding, + checked: this.forwarding, + }, + { + testid: 'lock-forwarding-checkbox', + label: PACKAGE_FORWARDING_ENFORCE_LABEL, + updateField: this.modelNames.lockForwarding, + checked: this.lockForwarding, + }, + ]; + }, + }, + methods: { + update(type, value) { + this.$emit('update', type, value); + }, + }, +}; +</script> + +<template> + <gl-form-group :label="label"> + <gl-form-checkbox + v-for="field in fields" + :key="field.testid" + :checked="field.checked" + :disabled="disabled" + :data-testid="field.testid" + @change="update(field.updateField, $event)" + > + <gl-sprintf :message="field.label"> + <template #packageType> + {{ label }} + </template> + </gl-sprintf> + </gl-form-checkbox> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue index f285dfc0755..36eb65c623b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue @@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui'; import { n__ } from '~/locale'; import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue'; +import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue'; import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue'; import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; @@ -11,6 +12,7 @@ export default { components: { GlAlert, PackagesSettings, + PackagesForwardingSettings, DependencyProxySettings, }, inject: ['groupPath'], @@ -82,6 +84,12 @@ export default { @error="handleError(2)" /> + <packages-forwarding-settings + :forward-settings="packageSettings" + @success="handleSuccess(2)" + @error="handleError(2)" + /> + <dependency-proxy-settings :dependency-proxy-settings="dependencyProxySettings" :dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy" diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue new file mode 100644 index 00000000000..b7d7f0aaca7 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue @@ -0,0 +1,190 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { + PACKAGE_FORWARDING_SETTINGS_HEADER, + PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, + PACKAGE_FORWARDING_FORM_BUTTON, + PACKAGE_FORWARDING_FIELDS, + MAVEN_FORWARDING_FIELDS, +} from '~/packages_and_registries/settings/group/constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql'; +import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; +import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; + +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; +import ForwardingSettings from '~/packages_and_registries/settings/group/components/forwarding_settings.vue'; + +export default { + name: 'PackageForwardingSettings', + i18n: { + PACKAGE_FORWARDING_FORM_BUTTON, + PACKAGE_FORWARDING_SETTINGS_HEADER, + PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, + }, + components: { + ForwardingSettings, + GlButton, + SettingsBlock, + }, + mixins: [glFeatureFlagsMixin()], + inject: ['groupPath'], + props: { + forwardSettings: { + type: Object, + required: true, + }, + }, + data() { + return { + mutationLoading: false, + workingCopy: { ...this.forwardSettings }, + }; + }, + computed: { + packageForwardingFields() { + const fields = PACKAGE_FORWARDING_FIELDS; + + if (this.glFeatures.mavenCentralRequestForwarding) { + return fields.concat(MAVEN_FORWARDING_FIELDS); + } + + return fields; + }, + isEdited() { + return !isEqual(this.forwardSettings, this.workingCopy); + }, + isDisabled() { + return !this.isEdited || this.mutationLoading; + }, + npmMutation() { + if (this.workingCopy.npmPackageRequestsForwardingLocked) { + return {}; + } + + return { + npmPackageRequestsForwarding: this.workingCopy.npmPackageRequestsForwarding, + lockNpmPackageRequestsForwarding: this.workingCopy.lockNpmPackageRequestsForwarding, + }; + }, + pypiMutation() { + if (this.workingCopy.pypiPackageRequestsForwardingLocked) { + return {}; + } + + return { + pypiPackageRequestsForwarding: this.workingCopy.pypiPackageRequestsForwarding, + lockPypiPackageRequestsForwarding: this.workingCopy.lockPypiPackageRequestsForwarding, + }; + }, + mavenMutation() { + if (this.workingCopy.mavenPackageRequestsForwardingLocked) { + return {}; + } + + return { + mavenPackageRequestsForwarding: this.workingCopy.mavenPackageRequestsForwarding, + lockMavenPackageRequestsForwarding: this.workingCopy.lockMavenPackageRequestsForwarding, + }; + }, + mutationVariables() { + return { + ...this.npmMutation, + ...this.pypiMutation, + ...this.mavenMutation, + }; + }, + }, + watch: { + forwardSettings(newValue) { + this.workingCopy = { ...newValue }; + }, + }, + methods: { + isForwardingFieldsDisabled(fields) { + const isLocked = fields?.modelNames?.isLocked; + + return this.mutationLoading || this.workingCopy[isLocked]; + }, + forwardingFieldsForwarding(fields) { + const forwarding = fields?.modelNames?.forwarding; + + return this.workingCopy[forwarding]; + }, + forwardingFieldsLockForwarding(fields) { + const lockForwarding = fields?.modelNames?.lockForwarding; + + return this.workingCopy[lockForwarding]; + }, + async submit() { + this.mutationLoading = true; + try { + const { data } = await this.$apollo.mutate({ + mutation: updateNamespacePackageSettings, + variables: { + input: { + namespacePath: this.groupPath, + ...this.mutationVariables, + }, + }, + update: updateGroupPackageSettings(this.groupPath), + optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({ + ...this.forwardSettings, + ...this.mutationVariables, + }), + }); + + if (data.updateNamespacePackageSettings?.errors?.length > 0) { + throw new Error(); + } else { + this.$emit('success'); + } + } catch { + this.$emit('error'); + } finally { + this.mutationLoading = false; + } + }, + updateWorkingCopy(type, value) { + this.$set(this.workingCopy, type, value); + }, + }, +}; +</script> + +<template> + <settings-block> + <template #title> {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_HEADER }}</template> + <template #description> + <span data-testid="description"> + {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_DESCRIPTION }} + </span> + </template> + <template #default> + <form @submit.prevent="submit"> + <forwarding-settings + v-for="forwardingFields in packageForwardingFields" + :key="forwardingFields.label" + :data-testid="forwardingFields.testid" + :disabled="isForwardingFieldsDisabled(forwardingFields)" + :forwarding="forwardingFieldsForwarding(forwardingFields)" + :label="forwardingFields.label" + :lock-forwarding="forwardingFieldsLockForwarding(forwardingFields)" + :model-names="forwardingFields.modelNames" + @update="updateWorkingCopy" + /> + <gl-button + type="submit" + :disabled="isDisabled" + :loading="mutationLoading" + category="primary" + variant="confirm" + class="js-no-auto-disable gl-mr-4" + > + {{ $options.i18n.PACKAGE_FORWARDING_FORM_BUTTON }} + </gl-button> + </form> + </template> + </settings-block> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index 2dd6d3f76f6..c93cd7f7d78 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -7,6 +7,8 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__( ); export const PACKAGE_FORMATS_TABLE_HEADER = s__('PackageRegistry|Package formats'); export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven'); +export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm'); +export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI'); export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic'); export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates'); @@ -15,11 +17,65 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__( 'PackageRegistry|Publish packages if their name or version matches this regex.', ); +export const PACKAGE_FORWARDING_SETTINGS_HEADER = s__('PackageRegistry|Package forwarding'); +export const PACKAGE_FORWARDING_SETTINGS_DESCRIPTION = s__( + 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.', +); +export const PACKAGE_FORWARDING_CHECKBOX_LABEL = s__( + `PackageRegistry|Forward %{packageType} package requests`, +); +export const PACKAGE_FORWARDING_ENFORCE_LABEL = s__( + `PackageRegistry|Enforce %{packageType} setting for all subgroups`, +); + +const MAVEN_PACKAGE_REQUESTS_FORWARDING = 'mavenPackageRequestsForwarding'; +const LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING = 'lockMavenPackageRequestsForwarding'; +const MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'mavenPackageRequestsForwardingLocked'; +const NPM_PACKAGE_REQUESTS_FORWARDING = 'npmPackageRequestsForwarding'; +const LOCK_NPM_PACKAGE_REQUESTS_FORWARDING = 'lockNpmPackageRequestsForwarding'; +const NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'npmPackageRequestsForwardingLocked'; +const PYPI_PACKAGE_REQUESTS_FORWARDING = 'pypiPackageRequestsForwarding'; +const LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING = 'lockPypiPackageRequestsForwarding'; +const PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'pypiPackageRequestsForwardingLocked'; + +export const PACKAGE_FORWARDING_FORM_BUTTON = __('Save changes'); + export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy'); export const DEPENDENCY_PROXY_DESCRIPTION = s__( 'DependencyProxy|Enable the Dependency Proxy and settings for clearing the cache.', ); +export const PACKAGE_FORWARDING_FIELDS = [ + { + label: NPM_PACKAGE_FORMAT, + testid: 'npm', + modelNames: { + forwarding: NPM_PACKAGE_REQUESTS_FORWARDING, + lockForwarding: LOCK_NPM_PACKAGE_REQUESTS_FORWARDING, + isLocked: NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED, + }, + }, + { + label: PYPI_PACKAGE_FORMAT, + testid: 'pypi', + modelNames: { + forwarding: PYPI_PACKAGE_REQUESTS_FORWARDING, + lockForwarding: LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING, + isLocked: PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED, + }, + }, +]; + +export const MAVEN_FORWARDING_FIELDS = { + label: MAVEN_PACKAGE_FORMAT, + testid: 'maven', + modelNames: { + forwarding: MAVEN_PACKAGE_REQUESTS_FORWARDING, + lockForwarding: LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING, + isLocked: MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED, + }, +}; + // Parameters export const PACKAGES_DOCS_PATH = helpPagePath('user/packages/index'); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql new file mode 100644 index 00000000000..267e40263f2 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql @@ -0,0 +1,15 @@ +fragment PackageSettingsFields on PackageSettings { + mavenDuplicatesAllowed + mavenDuplicateExceptionRegex + genericDuplicatesAllowed + genericDuplicateExceptionRegex + mavenPackageRequestsForwarding + lockMavenPackageRequestsForwarding + mavenPackageRequestsForwardingLocked + npmPackageRequestsForwarding + lockNpmPackageRequestsForwarding + npmPackageRequestsForwardingLocked + pypiPackageRequestsForwarding + lockPypiPackageRequestsForwarding + pypiPackageRequestsForwardingLocked +} diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql index 5c245ff9453..5558cb66f42 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql @@ -1,10 +1,9 @@ +#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql" + mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsInput!) { updateNamespacePackageSettings(input: $input) { packageSettings { - mavenDuplicatesAllowed - mavenDuplicateExceptionRegex - genericDuplicatesAllowed - genericDuplicateExceptionRegex + ...PackageSettingsFields } errors } diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql index 404d9d26d49..82a282d6d81 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql @@ -1,3 +1,5 @@ +#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql" + query getGroupPackagesSettings($fullPath: ID!) { group(fullPath: $fullPath) { id @@ -9,10 +11,7 @@ query getGroupPackagesSettings($fullPath: ID!) { enabled } packageSettings { - mavenDuplicatesAllowed - mavenDuplicateExceptionRegex - genericDuplicatesAllowed - genericDuplicateExceptionRegex + ...PackageSettingsFields } } } diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index f3530b46845..ac5e0b28dd1 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -3,6 +3,7 @@ import BranchSortDropdown from '~/branches/branch_sort_dropdown'; import initDiverganceGraph from '~/branches/divergence_graph'; import initDeleteBranchButton from '~/branches/init_delete_branch_button'; import initDeleteBranchModal from '~/branches/init_delete_branch_modal'; +import initDeleteMergedBranches from '~/branches/init_delete_merged_branches'; const { divergingCountsEndpoint, defaultBranch } = document.querySelector( '.js-branch-list', @@ -11,6 +12,7 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector( initDiverganceGraph(divergingCountsEndpoint, defaultBranch); BranchSortDropdown(); initDeprecatedRemoveRowBehavior(); +initDeleteMergedBranches(); document .querySelectorAll('.js-delete-branch-button') diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue index f2782f96da1..f5e1525090e 100644 --- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue +++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue @@ -45,7 +45,7 @@ export default { </script> <template> - <nav class="search-filter"> + <nav data-testid="search-filter"> <gl-nav vertical pills> <gl-nav-item v-for="(item, scope, index) in navigation" diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js new file mode 100644 index 00000000000..fd96da5faf6 --- /dev/null +++ b/app/assets/javascripts/sentry/constants.js @@ -0,0 +1,43 @@ +import { __ } from '~/locale'; + +export const IGNORE_ERRORS = [ + // Random plugins/extensions + 'top.GLOBALS', + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + __("Can't find variable: ZiteReader"), + __('jigsaw is not defined'), + __('ComboSearch is not defined'), + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + // Facebook borked + 'fb_xd_fragment', + // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to + // reduce this. (thanks @acdha) + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage', +]; + +export const DENY_URLS = [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, +]; + +export const SAMPLE_RATE = 0.95; diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js index 8f3c4c644bf..4c5b8dbad5a 100644 --- a/app/assets/javascripts/sentry/sentry_config.js +++ b/app/assets/javascripts/sentry/sentry_config.js @@ -1,52 +1,11 @@ import * as Sentry from '@sentry/browser'; import $ from 'jquery'; import { __ } from '~/locale'; - -const IGNORE_ERRORS = [ - // Random plugins/extensions - 'top.GLOBALS', - // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html - 'originalCreateNotification', - 'canvas.contentDocument', - 'MyApp_RemoveAllHighlights', - 'http://tt.epicplay.com', - __("Can't find variable: ZiteReader"), - __('jigsaw is not defined'), - __('ComboSearch is not defined'), - 'http://loading.retry.widdit.com/', - 'atomicFindClose', - // Facebook borked - 'fb_xd_fragment', - // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to - // reduce this. (thanks @acdha) - 'bmi_SafeAddOnload', - 'EBCallBackMessageReceived', - // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx - 'conduitPage', -]; - -const BLACKLIST_URLS = [ - // Facebook flakiness - /graph\.facebook\.com/i, - // Facebook blocked - /connect\.facebook\.net\/en_US\/all\.js/i, - // Woopra flakiness - /eatdifferent\.com\.woopra-ns\.com/i, - /static\.woopra\.com\/js\/woopra\.js/i, - // Chrome extensions - /extensions\//i, - /^chrome:\/\//i, - // Other plugins - /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb - /webappstoolbarba\.texthelp\.com\//i, - /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, -]; - -const SAMPLE_RATE = 0.95; +import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants'; const SentryConfig = { IGNORE_ERRORS, - BLACKLIST_URLS, + BLACKLIST_URLS: DENY_URLS, SAMPLE_RATE, init(options = {}) { this.options = options; |