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:
Diffstat (limited to 'app/assets/javascripts/ci_variable_list/components')
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue139
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue132
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue159
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue62
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue58
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue6
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue232
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue116
-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.vue428
-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
12 files changed, 394 insertions, 1250 deletions
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
index 8d891ff1746..719696f682e 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
@@ -1,143 +1,36 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
-import { reportMessageToSentry } from '../utils';
+import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
import getAdminVariables from '../graphql/queries/variables.query.graphql';
-import {
- ADD_MUTATION_ACTION,
- DELETE_MUTATION_ACTION,
- UPDATE_MUTATION_ACTION,
- genericMutationErrorText,
- variableFetchErrorText,
-} from '../constants';
import addAdminVariable from '../graphql/mutations/admin_add_variable.mutation.graphql';
import deleteAdminVariable from '../graphql/mutations/admin_delete_variable.mutation.graphql';
import updateAdminVariable from '../graphql/mutations/admin_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
+ CiVariableShared,
},
- inject: ['endpoint'],
- data() {
- return {
- adminVariables: [],
- hasNextPage: false,
- isInitialLoading: true,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- };
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
},
- apollo: {
- adminVariables: {
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.ciVariables,
query: getAdminVariables,
- update(data) {
- return data?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
-
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- watchLoading(flag) {
- if (!flag) {
- this.isInitialLoading = false;
- }
- },
- },
- },
- computed: {
- isLoading() {
- return (
- (this.$apollo.queries.adminVariables.loading && this.isInitialLoading) ||
- this.isLoadingMoreItems
- );
},
},
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.adminVariables.fetchMore({
- variables: {
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
- },
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- variable,
- },
- });
-
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- } else {
- // The writing to cache for admin variable is not working
- // because there is no ID in the cache at the top level.
- // We therefore need to manually refetch.
- this.$apollo.queries.adminVariables.refetch();
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
- },
- },
- componentName: 'InstanceVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
- mutationData: {
- [ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteAdminVariable, name: 'deleteAdminVariable' },
- },
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
:are-scoped-variables-available="false"
- :is-loading="isLoading"
- :variables="adminVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="InstanceVariables"
+ :hide-environment-scope="true"
+ :mutation-data="$options.mutationData"
+ :refetch-after-mutation="true"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index 4af696b8dab..c8f5ac1736d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -1,143 +1,53 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '../utils';
-import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_GROUP_TYPE,
UPDATE_MUTATION_ACTION,
- genericMutationErrorText,
- variableFetchErrorText,
} from '../constants';
+import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '../graphql/mutations/group_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
+ CiVariableShared,
},
mixins: [glFeatureFlagsMixin()],
- inject: ['endpoint', 'groupPath', 'groupId'],
- data() {
- return {
- groupVariables: [],
- hasNextPage: false,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- };
- },
- apollo: {
- groupVariables: {
- query: getGroupVariables,
- variables() {
- return {
- fullPath: this.groupPath,
- };
- },
- update(data) {
- return data?.group?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.group?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- },
- },
+ inject: ['groupPath', 'groupId'],
computed: {
areScopedVariablesAvailable() {
return this.glFeatures.groupScopedCiVariables;
},
- isLoading() {
- return this.$apollo.queries.groupVariables.loading || this.isLoadingMoreItems;
- },
- },
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.groupVariables.fetchMore({
- variables: {
- fullPath: this.groupPath,
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
- },
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- fullPath: this.groupPath,
- groupId: convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId),
- variable,
- },
- });
-
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
+ graphqlId() {
+ return convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId);
},
},
- componentName: 'GroupVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
mutationData: {
- [ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteGroupVariable, name: 'deleteGroupVariable' },
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.group?.ciVariables,
+ query: getGroupVariables,
+ },
},
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
+ :id="graphqlId"
:are-scoped-variables-available="areScopedVariablesAvailable"
- :is-loading="isLoading"
- :variables="groupVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="GroupVariables"
+ :full-path="groupPath"
+ :mutation-data="$options.mutationData"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 6bd549817f8..2c4818e20c1 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -1,160 +1,55 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
-import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
-import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_PROJECT_TYPE,
UPDATE_MUTATION_ACTION,
- environmentFetchErrorText,
- genericMutationErrorText,
- variableFetchErrorText,
} from '../constants';
+import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
import addProjectVariable from '../graphql/mutations/project_add_variable.mutation.graphql';
import deleteProjectVariable from '../graphql/mutations/project_delete_variable.mutation.graphql';
import updateProjectVariable from '../graphql/mutations/project_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
- },
- inject: ['endpoint', 'projectFullPath', 'projectId'],
- data() {
- return {
- hasNextPage: false,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- projectEnvironments: [],
- projectVariables: [],
- };
- },
- apollo: {
- projectEnvironments: {
- query: getProjectEnvironments,
- variables() {
- return {
- fullPath: this.projectFullPath,
- };
- },
- update(data) {
- return mapEnvironmentNames(data?.project?.environments?.nodes);
- },
- error() {
- createAlert({ message: environmentFetchErrorText });
- },
- },
- projectVariables: {
- query: getProjectVariables,
- variables() {
- return {
- after: null,
- fullPath: this.projectFullPath,
- };
- },
- update(data) {
- return data?.project?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.project?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- },
+ CiVariableShared,
},
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['projectFullPath', 'projectId'],
computed: {
- isLoading() {
- return (
- this.$apollo.queries.projectVariables.loading ||
- this.$apollo.queries.projectEnvironments.loading ||
- this.isLoadingMoreItems
- );
+ graphqlId() {
+ return convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId);
},
},
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.projectVariables.fetchMore({
- variables: {
- fullPath: this.projectFullPath,
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.project?.ciVariables,
+ query: getProjectVariables,
},
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- fullPath: this.projectFullPath,
- projectId: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId),
- variable,
- },
- });
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
+ environments: {
+ lookup: (data) => data?.project?.environments,
+ query: getProjectEnvironments,
},
},
- componentName: 'ProjectVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
- mutationData: {
- [ADD_MUTATION_ACTION]: { action: addProjectVariable, name: 'addProjectVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateProjectVariable, name: 'updateProjectVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteProjectVariable, name: 'deleteProjectVariable' },
- },
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
+ :id="graphqlId"
:are-scoped-variables-available="true"
- :environments="projectEnvironments"
- :is-loading="isLoading"
- :variables="projectVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="ProjectVariables"
+ :full-path="projectFullPath"
+ :mutation-data="$options.mutationData"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 56c1804910a..94f8cb9e906 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
@@ -86,6 +86,11 @@ export default {
required: false,
default: () => [],
},
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
mode: {
type: String,
required: true,
@@ -293,10 +298,11 @@ export default {
v-model="variable.value"
:state="variableValidationState"
rows="3"
- max-rows="6"
+ 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>
@@ -309,33 +315,35 @@ export default {
/>
</gl-form-group>
- <gl-form-group
- label-for="ci-variable-env"
- class="gl-w-half"
- 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="areScopedVariablesAvailable"
- class="gl-w-full"
- :selected-environment-scope="variable.environmentScope"
- :environments="joinedEnvironments"
- @select-environment="setEnvironmentScope"
- @create-environment-scope="createEnvironmentScope"
- />
+ <template v-if="!hideEnvironmentScope">
+ <gl-form-group
+ label-for="ci-variable-env"
+ class="gl-w-half"
+ 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="areScopedVariablesAvailable"
+ class="gl-w-full"
+ :selected-environment-scope="variable.environmentScope"
+ :environments="joinedEnvironments"
+ @select-environment="setEnvironmentScope"
+ @create-environment-scope="createEnvironmentScope"
+ />
- <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly />
- </gl-form-group>
+ <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly />
+ </gl-form-group>
+ </template>
</div>
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
deleted file mode 100644
index 605da5d9352..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- maxTextLength: 95,
- components: {
- GlPopover,
- GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- target: {
- type: String,
- required: true,
- },
- value: {
- type: String,
- required: true,
- },
- tooltipText: {
- type: String,
- required: true,
- },
- },
- computed: {
- displayValue() {
- if (this.value.length > this.$options.maxTextLength) {
- return `${this.value.substring(0, this.$options.maxTextLength)}...`;
- }
- return this.value;
- },
- },
-};
-</script>
-
-<template>
- <div id="popover-container">
- <gl-popover :target="target" placement="top" container="popover-container">
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-word-break-all"
- >
- <div class="ci-popover-value gl-pr-3">
- {{ displayValue }}
- </div>
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- :title="tooltipText"
- :data-clipboard-text="value"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </gl-popover>
- </div>
-</template>
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 81e3a983ea3..94fd6c3892c 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
@@ -19,6 +19,11 @@ export default {
required: false,
default: () => [],
},
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isLoading: {
type: Boolean,
required: false,
@@ -78,6 +83,7 @@ export default {
v-if="showModal"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
+ :hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
:selected-variable="selectedVariable"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
new file mode 100644
index 00000000000..7ee250cea98
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
@@ -0,0 +1,232 @@
+<script>
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ environmentFetchErrorText,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '../constants';
+import CiVariableSettings from './ci_variable_settings.vue';
+
+export default {
+ components: {
+ CiVariableSettings,
+ },
+ inject: ['endpoint'],
+ props: {
+ areScopedVariablesAvailable: {
+ required: true,
+ type: Boolean,
+ },
+ componentName: {
+ required: true,
+ type: String,
+ },
+ fullPath: {
+ required: false,
+ type: String,
+ default: null,
+ },
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ id: {
+ required: false,
+ type: String,
+ default: null,
+ },
+ mutationData: {
+ required: true,
+ type: Object,
+ validator: (obj) => {
+ const hasValidKeys = Object.keys(obj).includes(
+ ADD_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ );
+
+ const hasValidValues = Object.values(obj).reduce((acc, val) => {
+ return acc && typeof val === 'object';
+ }, true);
+
+ return hasValidKeys && hasValidValues;
+ },
+ },
+ refetchAfterMutation: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ queryData: {
+ required: true,
+ type: Object,
+ validator: (obj) => {
+ const { ciVariables, environments } = obj;
+ const hasCiVariablesKey = Boolean(ciVariables);
+ let hasCorrectEnvData = true;
+
+ const hasCorrectVariablesData =
+ typeof ciVariables?.lookup === 'function' && typeof ciVariables.query === 'object';
+
+ if (environments) {
+ hasCorrectEnvData =
+ typeof environments?.lookup === 'function' && typeof environments.query === 'object';
+ }
+
+ return hasCiVariablesKey && hasCorrectVariablesData && hasCorrectEnvData;
+ },
+ },
+ },
+ data() {
+ return {
+ ciVariables: [],
+ hasNextPage: false,
+ isInitialLoading: true,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ ciVariables: {
+ query() {
+ return this.queryData.ciVariables.query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath || undefined,
+ };
+ },
+ update(data) {
+ return this.queryData.ciVariables.lookup(data)?.nodes || [];
+ },
+ result({ data }) {
+ this.pageInfo = this.queryData.ciVariables.lookup(data)?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
+ error() {
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
+ },
+ watchLoading(flag) {
+ if (!flag) {
+ this.isInitialLoading = false;
+ }
+ },
+ },
+ environments: {
+ query() {
+ return this.queryData?.environments?.query || {};
+ },
+ skip() {
+ return !this.queryData?.environments?.query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return mapEnvironmentNames(this.queryData.environments.lookup(data)?.nodes);
+ },
+ error() {
+ createAlert({ message: environmentFetchErrorText });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return (
+ (this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
+ this.$apollo.queries.environments.loading ||
+ this.isLoadingMoreItems
+ );
+ },
+ },
+ methods: {
+ addVariable(variable) {
+ this.variableMutation(ADD_MUTATION_ACTION, variable);
+ },
+ deleteVariable(variable) {
+ this.variableMutation(DELETE_MUTATION_ACTION, variable);
+ },
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.ciVariables.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
+ updateVariable(variable) {
+ this.variableMutation(UPDATE_MUTATION_ACTION, variable);
+ },
+ async variableMutation(mutationAction, variable) {
+ try {
+ const currentMutation = this.mutationData[mutationAction];
+
+ const { data } = await this.$apollo.mutate({
+ mutation: currentMutation,
+ variables: {
+ endpoint: this.endpoint,
+ fullPath: this.fullPath || undefined,
+ id: this.id || undefined,
+ variable,
+ },
+ });
+
+ if (data.ciVariableMutation?.errors?.length) {
+ const { errors } = data.ciVariableMutation;
+ createAlert({ message: errors[0] });
+ } else if (this.refetchAfterMutation) {
+ // The writing to cache for admin variable is not working
+ // because there is no ID in the cache at the top level.
+ // We therefore need to manually refetch.
+ this.$apollo.queries.ciVariables.refetch();
+ }
+ } catch (e) {
+ createAlert({ message: genericMutationErrorText });
+ }
+ },
+ },
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
+};
+</script>
+
+<template>
+ <ci-variable-settings
+ :are-scoped-variables-available="areScopedVariablesAvailable"
+ :hide-environment-scope="hideEnvironmentScope"
+ :is-loading="isLoading"
+ :variables="ciVariables"
+ :environments="environments"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
+ @update-variable="updateVariable"
+ />
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 959ef6864fb..3cdcb68e919 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -1,71 +1,49 @@
<script>
-import {
- GlButton,
- GlIcon,
- GlLoadingIcon,
- GlModalDirective,
- GlTable,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlModalDirective, GlTable, GlTooltipDirective } from '@gitlab/ui';
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, variableText } from '../constants';
import { convertEnvironmentScope } from '../utils';
-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: 'variableType',
label: s__('CiVariables|Type'),
- customStyle: { width: '70px' },
+ thClass: 'gl-w-10p',
},
{
key: 'key',
label: s__('CiVariables|Key'),
tdClass: 'text-plain',
sortable: true,
- customStyle: { width: '40%' },
},
{
key: 'value',
label: s__('CiVariables|Value'),
- customStyle: { width: '40%' },
+ thClass: 'gl-w-15p',
},
{
- key: 'protected',
- label: s__('CiVariables|Protected'),
- customStyle: { width: '100px' },
- },
- {
- key: 'masked',
- label: s__('CiVariables|Masked'),
- customStyle: { width: '100px' },
+ key: 'options',
+ label: s__('CiVariables|Options'),
+ thClass: 'gl-w-10p',
},
{
key: 'environmentScope',
label: s__('CiVariables|Environments'),
- customStyle: { width: '20%' },
},
{
key: 'actions',
label: '',
tdClass: 'text-right',
- customStyle: { width: '35px' },
+ thClass: 'gl-w-5p',
},
],
components: {
- CiVariablePopover,
GlButton,
- GlIcon,
GlLoadingIcon,
GlTable,
- TooltipOnTruncate,
},
directives: {
GlModalDirective,
@@ -97,6 +75,13 @@ export default {
fields() {
return this.$options.fields;
},
+ variablesWithOptions() {
+ return this.variables?.map((item, index) => ({
+ ...item,
+ options: this.getOptions(item),
+ index,
+ }));
+ },
},
methods: {
convertEnvironmentScopeValue(env) {
@@ -108,8 +93,18 @@ export default {
toggleHiddenState() {
this.areValuesHidden = !this.areValuesHidden;
},
- setSelectedVariable(variable = null) {
- this.$emit('set-selected-variable', variable);
+ setSelectedVariable(index = -1) {
+ this.$emit('set-selected-variable', this.variables[index] ?? null);
+ },
+ getOptions(item) {
+ const options = [];
+ if (item.protected) {
+ options.push(s__('CiVariables|Protected'));
+ }
+ if (item.masked) {
+ options.push(s__('CiVariables|Masked'));
+ }
+ return options.join(', ');
},
},
};
@@ -121,7 +116,7 @@ export default {
<gl-table
v-else
:fields="fields"
- :items="variables"
+ :items="variablesWithOptions"
tbody-tr-class="js-ci-variable-row"
data-qa-selector="ci_variable_table_content"
sort-by="key"
@@ -137,23 +132,22 @@ export default {
<col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
</template>
<template #cell(variableType)="{ item }">
- <div class="gl-pt-2">
- {{ generateTypeText(item) }}
- </div>
+ {{ generateTypeText(item) }}
</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>
+ <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')"
@@ -161,8 +155,10 @@ export default {
</div>
</template>
<template #cell(value)="{ item }">
- <div class="gl-display-flex gl-align-items-center">
- <span v-if="areValuesHidden" data-testid="hiddenValue">*********************</span>
+ <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="areValuesHidden" data-testid="hiddenValue">*****</span>
<span
v-else
:id="`ci-variable-value-${item.id}`"
@@ -174,31 +170,33 @@ export default {
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(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 #cell(options)="{ item }">
+ <span>{{ item.options }}</span>
</template>
<template #cell(environmentScope)="{ item }">
- <div class="gl-display-flex">
+ <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-text-truncate"
+ class="gl-display-inline-block gl-max-w-full gl-word-break-word"
>{{ convertEnvironmentScopeValue(item.environmentScope) }}</span
>
- <ci-variable-popover
- :target="`ci-variable-env-${item.id}`"
- :value="convertEnvironmentScopeValue(item.environmentScope)"
- :tooltip-text="__('Copy environment')"
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ class="gl-my-n3 gl-ml-2"
+ :title="__('Copy environment')"
+ :data-clipboard-text="convertEnvironmentScopeValue(item.environmentScope)"
+ :aria-label="__('Copy to clipboard')"
/>
</div>
</template>
@@ -208,7 +206,7 @@ export default {
icon="pencil"
:aria-label="__('Edit')"
data-qa-selector="edit_ci_variable_button"
- @click="setSelectedVariable(item)"
+ @click="setSelectedVariable(item.index)"
/>
</template>
<template #empty>
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 1fbe52388c9..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
+++ /dev/null
@@ -1,428 +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="6"
- data-testid="pipeline-form-ci-variable-value"
- 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>
- <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 f078234829a..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<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>