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>2020-04-20 21:38:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-20 21:38:24 +0300
commit983a0bba5d2a042c4a3bbb22432ec192c7501d82 (patch)
treeb153cd387c14ba23bd5a07514c7c01fddf6a78a0 /app/assets/javascripts/ci_variable_list
parenta2bddee2cdb38673df0e004d5b32d9f77797de64 (diff)
Add latest changes from gitlab-org/gitlab@12-10-stable-ee
Diffstat (limited to 'app/assets/javascripts/ci_variable_list')
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_key_field.vue169
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js29
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue100
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js5
4 files changed, 278 insertions, 25 deletions
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue b/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue
new file mode 100644
index 00000000000..f5c2cc57f3f
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue
@@ -0,0 +1,169 @@
+<script>
+import { uniqueId } from 'lodash';
+import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
+
+export default {
+ name: 'CiKeyField',
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ },
+ model: {
+ prop: 'value',
+ event: 'input',
+ },
+ props: {
+ tokenList: {
+ type: Array,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ results: [],
+ arrowCounter: -1,
+ userDismissedResults: false,
+ suggestionsId: uniqueId('token-suggestions-'),
+ };
+ },
+ computed: {
+ showAutocomplete() {
+ return this.showSuggestions ? 'off' : 'on';
+ },
+ showSuggestions() {
+ return this.results.length > 0;
+ },
+ },
+ mounted() {
+ document.addEventListener('click', this.handleClickOutside);
+ },
+ destroyed() {
+ document.removeEventListener('click', this.handleClickOutside);
+ },
+ methods: {
+ closeSuggestions() {
+ this.results = [];
+ this.arrowCounter = -1;
+ },
+ handleClickOutside(event) {
+ if (!this.$el.contains(event.target)) {
+ this.closeSuggestions();
+ }
+ },
+ onArrowDown() {
+ const newCount = this.arrowCounter + 1;
+
+ if (newCount >= this.results.length) {
+ this.arrowCounter = 0;
+ return;
+ }
+
+ this.arrowCounter = newCount;
+ },
+ onArrowUp() {
+ const newCount = this.arrowCounter - 1;
+
+ if (newCount < 0) {
+ this.arrowCounter = this.results.length - 1;
+ return;
+ }
+
+ this.arrowCounter = newCount;
+ },
+ onEnter() {
+ const currentToken = this.results[this.arrowCounter] || this.value;
+ this.selectToken(currentToken);
+ },
+ onEsc() {
+ if (!this.showSuggestions) {
+ this.$emit('input', '');
+ }
+ this.closeSuggestions();
+ this.userDismissedResults = true;
+ },
+ onEntry(value) {
+ this.$emit('input', value);
+ this.userDismissedResults = false;
+
+ // short circuit so that we don't false match on empty string
+ if (value.length < 1) {
+ this.closeSuggestions();
+ return;
+ }
+
+ const filteredTokens = this.tokenList.filter(token =>
+ token.toLowerCase().includes(value.toLowerCase()),
+ );
+
+ if (filteredTokens.length) {
+ this.openSuggestions(filteredTokens);
+ } else {
+ this.closeSuggestions();
+ }
+ },
+ openSuggestions(filteredResults) {
+ this.results = filteredResults;
+ },
+ selectToken(value) {
+ this.$emit('input', value);
+ this.closeSuggestions();
+ this.$emit('key-selected');
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="dropdown position-relative" role="combobox" aria-owns="token-suggestions">
+ <gl-form-group :label="__('Key')" label-for="ci-variable-key">
+ <gl-form-input
+ id="ci-variable-key"
+ :value="value"
+ type="text"
+ role="searchbox"
+ class="form-control pl-2 js-env-input"
+ :autocomplete="showAutocomplete"
+ aria-autocomplete="list"
+ aria-controls="token-suggestions"
+ aria-haspopup="listbox"
+ :aria-expanded="showSuggestions"
+ data-qa-selector="ci_variable_key_field"
+ @input="onEntry"
+ @keydown.down="onArrowDown"
+ @keydown.up="onArrowUp"
+ @keydown.enter.prevent="onEnter"
+ @keydown.esc.stop="onEsc"
+ @keydown.tab="closeSuggestions"
+ />
+ </gl-form-group>
+
+ <div
+ v-show="showSuggestions && !userDismissedResults"
+ id="ci-variable-dropdown"
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"
+ :class="{ 'd-block': showSuggestions }"
+ >
+ <div class="dropdown-content">
+ <ul :id="suggestionsId">
+ <li
+ v-for="(result, i) in results"
+ :key="i"
+ role="option"
+ :class="{ 'gl-bg-gray-100': i === arrowCounter }"
+ :aria-selected="i === arrowCounter"
+ >
+ <gl-button tabindex="-1" class="btn-transparent pl-2" @click="selectToken(result)">{{
+ result
+ }}</gl-button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js b/app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js
new file mode 100644
index 00000000000..9022bf51514
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js
@@ -0,0 +1,29 @@
+import { __ } from '~/locale';
+
+import { AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY } from '../constants';
+
+export const awsTokens = {
+ [AWS_ACCESS_KEY_ID]: {
+ name: AWS_ACCESS_KEY_ID,
+ /* Checks for exactly twenty characters that match key.
+ Based on greps suggested by Amazon at:
+ https://aws.amazon.com/blogs/security/a-safer-way-to-distribute-aws-credentials-to-ec2/
+ */
+ validation: val => /^[A-Za-z0-9]{20}$/.test(val),
+ invalidMessage: __('This variable does not match the expected pattern.'),
+ },
+ [AWS_DEFAULT_REGION]: {
+ name: AWS_DEFAULT_REGION,
+ },
+ [AWS_SECRET_ACCESS_KEY]: {
+ name: AWS_SECRET_ACCESS_KEY,
+ /* Checks for exactly forty characters that match secret.
+ Based on greps suggested by Amazon at:
+ https://aws.amazon.com/blogs/security/a-safer-way-to-distribute-aws-credentials-to-ec2/
+ */
+ validation: val => /^[A-Za-z0-9/+=]{40}$/.test(val),
+ invalidMessage: __('This variable does not match the expected pattern.'),
+ },
+};
+
+export const awsTokenList = Object.keys(awsTokens);
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 316408adfb2..8f5acd4a0a0 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
@@ -1,8 +1,4 @@
<script>
-import { __ } from '~/locale';
-import { mapActions, mapState } from 'vuex';
-import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
-import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import {
GlDeprecatedButton,
GlModal,
@@ -14,11 +10,19 @@ import {
GlLink,
GlIcon,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
+import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
+import CiKeyField from './ci_key_field.vue';
+import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
components: {
CiEnvironmentsDropdown,
+ CiKeyField,
GlDeprecatedButton,
GlModal,
GlFormSelect,
@@ -29,6 +33,9 @@ export default {
GlLink,
GlIcon,
},
+ mixins: [glFeatureFlagsMixin()],
+ tokens: awsTokens,
+ tokenList: awsTokenList,
computed: {
...mapState([
'projectId',
@@ -41,23 +48,24 @@ export default {
'selectedEnvironment',
]),
canSubmit() {
- if (this.variableData.masked && this.maskedState === false) {
- return false;
- }
- return this.variableData.key !== '' && this.variableData.secret_value !== '';
+ return (
+ this.variableValidationState &&
+ this.variableData.key !== '' &&
+ this.variableData.secret_value !== ''
+ );
},
canMask() {
const regex = RegExp(this.maskableRegex);
return regex.test(this.variableData.secret_value);
},
displayMaskedError() {
- return !this.canMask && this.variableData.masked && this.variableData.secret_value !== '';
+ return !this.canMask && this.variableData.masked;
},
maskedState() {
if (this.displayMaskedError) {
return false;
}
- return null;
+ return true;
},
variableData() {
return this.variableBeingEdited || this.variable;
@@ -66,7 +74,41 @@ export default {
return this.variableBeingEdited ? __('Update variable') : __('Add variable');
},
maskedFeedback() {
- return __('This variable can not be masked');
+ return this.displayMaskedError ? __('This variable can not be masked.') : '';
+ },
+ tokenValidationFeedback() {
+ const tokenSpecificFeedback = this.$options.tokens?.[this.variableData.key]?.invalidMessage;
+ if (!this.tokenValidationState && tokenSpecificFeedback) {
+ return tokenSpecificFeedback;
+ }
+ return '';
+ },
+ tokenValidationState() {
+ // If the feature flag is off, do not validate. Remove when flag is removed.
+ if (!this.glFeatures.ciKeyAutocomplete) {
+ return true;
+ }
+
+ const validator = this.$options.tokens?.[this.variableData.key]?.validation;
+
+ if (validator) {
+ return validator(this.variableData.secret_value);
+ }
+
+ return true;
+ },
+ variableValidationFeedback() {
+ return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
+ },
+ variableValidationState() {
+ if (
+ this.variableData.secret_value === '' ||
+ (this.tokenValidationState && this.maskedState)
+ ) {
+ return true;
+ }
+
+ return false;
},
},
methods: {
@@ -82,14 +124,13 @@ export default {
'resetSelectedEnvironment',
'setSelectedEnvironment',
]),
- updateOrAddVariable() {
- if (this.variableBeingEdited) {
- this.updateVariable(this.variableBeingEdited);
- } else {
- this.addVariable();
- }
+ deleteVarAndClose() {
+ this.deleteVariable(this.variableBeingEdited);
this.hideModal();
},
+ hideModal() {
+ this.$refs.modal.hide();
+ },
resetModalHandler() {
if (this.variableBeingEdited) {
this.resetEditing();
@@ -98,11 +139,12 @@ export default {
}
this.resetSelectedEnvironment();
},
- hideModal() {
- this.$refs.modal.hide();
- },
- deleteVarAndClose() {
- this.deleteVariable(this.variableBeingEdited);
+ updateOrAddVariable() {
+ if (this.variableBeingEdited) {
+ this.updateVariable(this.variableBeingEdited);
+ } else {
+ this.addVariable();
+ }
this.hideModal();
},
},
@@ -119,7 +161,13 @@ export default {
@hidden="resetModalHandler"
>
<form>
- <gl-form-group :label="__('Key')" label-for="ci-variable-key">
+ <ci-key-field
+ v-if="glFeatures.ciKeyAutocomplete"
+ v-model="variableData.key"
+ :token-list="$options.tokenList"
+ />
+
+ <gl-form-group v-else :label="__('Key')" label-for="ci-variable-key">
<gl-form-input
id="ci-variable-key"
v-model="variableData.key"
@@ -130,12 +178,14 @@ export default {
<gl-form-group
:label="__('Value')"
label-for="ci-variable-value"
- :state="maskedState"
- :invalid-feedback="maskedFeedback"
+ :state="variableValidationState"
+ :invalid-feedback="variableValidationFeedback"
>
<gl-form-textarea
id="ci-variable-value"
+ ref="valueField"
v-model="variableData.secret_value"
+ :state="variableValidationState"
rows="3"
max-rows="6"
data-qa-selector="ci_variable_value_field"
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index d22138db102..5fe1e32e37e 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -14,3 +14,8 @@ export const types = {
fileType: 'file',
allEnvironmentsType: '*',
};
+
+// AWS TOKEN CONSTANTS
+export const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
+export const AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION';
+export const AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';