diff options
Diffstat (limited to 'app/assets/javascripts/ci_variable_list/components')
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> |