Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 14:10:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 14:10:13 +0300
commit0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch)
tree7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app/assets/javascripts/ci_variable_list
parent72123183a20411a36d607d70b12d57c484394c8e (diff)
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci_variable_list')
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue3
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue27
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue81
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue426
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue32
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue199
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js63
7 files changed, 803 insertions, 28 deletions
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 3af89dc4a2c..557a8d6b5ba 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
@@ -369,7 +369,7 @@ export default {
:href="awsTipLearnLink"
target="_blank"
category="secondary"
- variant="info"
+ variant="confirm"
class="gl-overflow-wrap-break"
>{{ __('Learn more about deploying to AWS') }}</gl-button
>
@@ -416,6 +416,7 @@ export default {
:disabled="!canSubmit"
variant="confirm"
category="primary"
+ data-testid="ciUpdateOrAddVariableBtn"
data-qa-selector="ci_variable_save_button"
@click="updateOrAddVariable"
>{{ modalActionText }}
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 12bc5ad3549..4cc00eb01d9 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -1,32 +1,9 @@
<script>
-import { mapState, mapActions } from 'vuex';
-import CiVariableModal from './ci_variable_modal.vue';
-import CiVariableTable from './ci_variable_table.vue';
-
-export default {
- components: {
- CiVariableModal,
- CiVariableTable,
- },
- computed: {
- ...mapState(['isGroup']),
- },
- mounted() {
- if (!this.isGroup) {
- this.fetchEnvironments();
- }
- },
- methods: {
- ...mapActions(['fetchEnvironments']),
- },
-};
+export default {};
</script>
<template>
<div class="row">
- <div class="col-lg-12">
- <ci-variable-table />
- <ci-variable-modal />
- </div>
+ <div class="col-lg-12"></div>
</div>
</template>
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
new file mode 100644
index 00000000000..ecb39f214ec
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
@@ -0,0 +1,81 @@
+<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
new file mode 100644
index 00000000000..7dcc5ce42d7
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
@@ -0,0 +1,426 @@
+<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 CiEnvironmentsDropdown from './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: {
+ CiEnvironmentsDropdown,
+ 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-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="6"
+ data-qa-selector="ci_variable_value_field"
+ class="gl-font-monospace!"
+ />
+ </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>
+ <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
new file mode 100644
index 00000000000..9acc9fbffb6
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
@@ -0,0 +1,32 @@
+<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']),
+ },
+ mounted() {
+ if (!this.isGroup) {
+ 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
new file mode 100644
index 00000000000..f078234829a
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
@@ -0,0 +1,199 @@
+<script>
+import { GlTable, GlButton, GlModalDirective, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { s__, __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
+import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
+import CiVariablePopover from './ci_variable_popover.vue';
+
+export default {
+ modalId: ADD_CI_VARIABLE_MODAL_ID,
+ trueIcon: 'mobile-issue-close',
+ falseIcon: 'close',
+ iconSize: 16,
+ fields: [
+ {
+ key: 'variable_type',
+ label: s__('CiVariables|Type'),
+ customStyle: { width: '70px' },
+ },
+ {
+ key: 'key',
+ label: s__('CiVariables|Key'),
+ tdClass: 'text-plain',
+ sortable: true,
+ customStyle: { width: '40%' },
+ },
+ {
+ key: 'value',
+ label: s__('CiVariables|Value'),
+ customStyle: { width: '40%' },
+ },
+ {
+ key: 'protected',
+ label: s__('CiVariables|Protected'),
+ customStyle: { width: '100px' },
+ },
+ {
+ key: 'masked',
+ label: s__('CiVariables|Masked'),
+ customStyle: { width: '100px' },
+ },
+ {
+ key: 'environment_scope',
+ label: s__('CiVariables|Environments'),
+ customStyle: { width: '20%' },
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'text-right',
+ customStyle: { width: '35px' },
+ },
+ ],
+ components: {
+ CiVariablePopover,
+ GlButton,
+ GlIcon,
+ GlTable,
+ TooltipOnTruncate,
+ },
+ 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;
+ },
+ },
+ mounted() {
+ this.fetchVariables();
+ },
+ methods: {
+ ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
+ },
+};
+</script>
+
+<template>
+ <div class="ci-variable-table" data-testid="ci-variable-table">
+ <gl-table
+ :fields="fields"
+ :items="variables"
+ 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-center">
+ <tooltip-on-truncate :title="item.key" truncate-target="child">
+ <span
+ :id="`ci-variable-key-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.key }}</span
+ >
+ </tooltip-on-truncate>
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ :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-center">
+ <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"
+ :title="__('Copy value')"
+ :data-clipboard-text="item.value"
+ :aria-label="__('Copy to clipboard')"
+ />
+ </div>
+ </template>
+ <template #cell(protected)="{ item }">
+ <gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
+ <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
+ </template>
+ <template #cell(masked)="{ item }">
+ <gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
+ <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
+ </template>
+ <template #cell(environment_scope)="{ item }">
+ <div class="gl-display-flex">
+ <span
+ :id="`ci-variable-env-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ >{{ item.environment_scope }}</span
+ >
+ <ci-variable-popover
+ :target="`ci-variable-env-${item.id}`"
+ :value="item.environment_scope"
+ :tooltip-text="__('Copy environment')"
+ />
+ </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="editVariable(item)"
+ />
+ </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 f771751194c..2b54af6a2a4 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -1,10 +1,63 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiVariableSettings from './components/ci_variable_settings.vue';
+import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
const {
+ awsLogoSvgPath,
+ awsTipCommandsLink,
+ awsTipDeployLink,
+ awsTipLearnLink,
+ containsVariableReferenceLink,
+ environmentScopeLink,
+ group,
+ maskedEnvironmentVariablesLink,
+ maskableRegex,
+ projectFullPath,
+ projectId,
+ protectedByDefault,
+ protectedEnvironmentVariablesLink,
+ } = containerEl.dataset;
+
+ const isGroup = parseBoolean(group);
+ const isProtectedByDefault = parseBoolean(protectedByDefault);
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ awsLogoSvgPath,
+ awsTipCommandsLink,
+ awsTipDeployLink,
+ awsTipLearnLink,
+ containsVariableReferenceLink,
+ environmentScopeLink,
+ isGroup,
+ isProtectedByDefault,
+ maskedEnvironmentVariablesLink,
+ maskableRegex,
+ projectFullPath,
+ projectId,
+ protectedEnvironmentVariablesLink,
+ },
+ render(createElement) {
+ return createElement(CiVariableSettings);
+ },
+ });
+};
+
+const mountLegacyCiVariableListApp = (containerEl) => {
+ const {
endpoint,
projectId,
group,
@@ -42,7 +95,7 @@ const mountCiVariableListApp = (containerEl) => {
el: containerEl,
store,
render(createElement) {
- return createElement(CiVariableSettings);
+ return createElement(LegacyCiVariableSettings);
},
});
};
@@ -50,5 +103,11 @@ const mountCiVariableListApp = (containerEl) => {
export default (containerId = 'js-ci-project-variables') => {
const el = document.getElementById(containerId);
- return !el ? {} : mountCiVariableListApp(el);
+ if (el) {
+ if (gon.features?.ciVariableSettingsGraphql) {
+ mountCiVariableListApp(el);
+ } else {
+ mountLegacyCiVariableListApp(el);
+ }
+ }
};