diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-16 21:12:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-16 21:12:52 +0300 |
commit | 8a9790b0db723db32f8dff511ee032e5e8e3b583 (patch) | |
tree | 8173501b91ea0ada6a68d656786867b2abcc97f9 /app/assets | |
parent | 7212129029f4e7e68614066cc43802faba42c554 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
14 files changed, 293 insertions, 24 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 new file mode 100644 index 00000000000..83bad9eb518 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue @@ -0,0 +1,101 @@ +<script> +import createFlash from '~/flash'; +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'; + +export default { + components: { + ciVariableSettings, + }, + inject: ['endpoint'], + data() { + return { + adminVariables: [], + isInitialLoading: true, + }; + }, + apollo: { + adminVariables: { + query: getAdminVariables, + update(data) { + return data?.ciVariables?.nodes || []; + }, + error() { + createFlash({ message: variableFetchErrorText }); + }, + watchLoading(flag) { + if (!flag) { + this.isInitialLoading = false; + } + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.adminVariables.loading && this.isInitialLoading; + }, + }, + methods: { + addVariable(variable) { + this.variableMutation(ADD_MUTATION_ACTION, variable); + }, + deleteVariable(variable) { + this.variableMutation(DELETE_MUTATION_ACTION, variable); + }, + 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, + }, + }); + + const { errors } = data[currentMutation.name]; + + if (errors.length > 0) { + createFlash({ 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 { + createFlash({ message: genericMutationErrorText }); + } + }, + }, + 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 + :are-scoped-variables-available="false" + :is-loading="isLoading" + :variables="adminVariables" + @add-variable="addVariable" + @delete-variable="deleteVariable" + @update-variable="updateVariable" + /> +</template> diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue index 8ee7132bb25..c9002edc1ab 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue @@ -33,9 +33,9 @@ export default { }, filteredEnvironments() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.environments.filter((resultString) => - resultString.toLowerCase().includes(lowerCasedSearchTerm), - ); + return this.environments.filter((environment) => { + return environment.toLowerCase().includes(lowerCasedSearchTerm); + }); }, shouldRenderCreateButton() { return this.searchTerm && !this.environments.includes(this.searchTerm); 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 dc57f3fe4ce..5ba63de8c96 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 @@ -33,7 +33,7 @@ import { VARIABLE_ACTIONS, variableOptions, } from '../constants'; - +import { createJoinedEnvironments } from '../utils'; import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; @@ -98,9 +98,15 @@ export default { required: false, default: () => {}, }, + variables: { + type: Array, + required: false, + default: () => [], + }, }, data() { return { + newEnvironments: [], isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', typeOptions: variableOptions, validationErrorEventProperty: '', @@ -128,6 +134,9 @@ export default { isTipVisible() { return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); }, + joinedEnvironments() { + return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments); + }, maskedFeedback() { return this.displayMaskedError ? __('This variable can not be masked.') : ''; }, @@ -176,7 +185,7 @@ export default { this.$emit('add-variable', this.variable); }, createEnvironmentScope(env) { - this.$emit('create-environment-scope', env); + this.newEnvironments.push(env); }, deleteVariable() { this.$emit('delete-variable', this.variable); @@ -314,7 +323,7 @@ export default { v-if="areScopedVariablesAvailable" class="gl-w-full" :selected-environment-scope="variable.environmentScope" - :environments="environments" + :environments="joinedEnvironments" @select-environment="setEnvironmentScope" @create-environment-scope="createEnvironmentScope" /> 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 29578c6f710..81e3a983ea3 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue @@ -1,6 +1,5 @@ <script> import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants'; -import { createJoinedEnvironments } from '../utils'; import CiVariableTable from './ci_variable_table.vue'; import CiVariableModal from './ci_variable_modal.vue'; @@ -17,7 +16,8 @@ export default { }, environments: { type: Array, - required: true, + required: false, + default: () => [], }, isLoading: { type: Boolean, @@ -36,9 +36,6 @@ export default { }; }, computed: { - joinedEnvironments() { - return createJoinedEnvironments(this.variables, this.environments); - }, showModal() { return VARIABLE_ACTIONS.includes(this.mode); }, @@ -80,7 +77,8 @@ export default { <ci-variable-modal v-if="showModal" :are-scoped-variables-available="areScopedVariablesAvailable" - :environments="joinedEnvironments" + :environments="environments" + :variables="variables" :mode="mode" :selected-variable="selectedVariable" @add-variable="addVariable" diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js index e42a728a44e..5d22974ffbb 100644 --- a/app/assets/javascripts/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci_variable_list/constants.js @@ -47,6 +47,13 @@ export const defaultVariableState = { variableType: types.variableType, }; +// eslint-disable-next-line @gitlab/require-i18n-strings +export const groupString = 'Group'; +// eslint-disable-next-line @gitlab/require-i18n-strings +export const instanceString = 'Instance'; +// eslint-disable-next-line @gitlab/require-i18n-strings +export const projectString = 'Instance'; + export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed'; export const AWS_TIP_MESSAGE = __( '%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.', diff --git a/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql new file mode 100644 index 00000000000..a28ca4eebc9 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql @@ -0,0 +1,7 @@ +fragment BaseCiVariable on CiVariable { + __typename + id + key + value + variableType +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql new file mode 100644 index 00000000000..eba4b0c32f8 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql @@ -0,0 +1,16 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation addAdminVariable($variable: CiVariable!, $endpoint: String!) { + addAdminVariable(variable: $variable, endpoint: $endpoint) @client { + ciVariables { + nodes { + ...BaseCiVariable + ... on CiInstanceVariable { + protected + masked + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql new file mode 100644 index 00000000000..96eb8c794bc --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql @@ -0,0 +1,16 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation deleteAdminVariable($variable: CiVariable!, $endpoint: String!) { + deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client { + ciVariables { + nodes { + ...BaseCiVariable + ... on CiInstanceVariable { + protected + masked + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql new file mode 100644 index 00000000000..c0388507bb8 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql @@ -0,0 +1,16 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation updateAdminVariable($variable: CiVariable!, $endpoint: String!) { + updateAdminVariable(variable: $variable, endpoint: $endpoint) @client { + ciVariables { + nodes { + ...BaseCiVariable + ... on CiInstanceVariable { + protected + masked + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql new file mode 100644 index 00000000000..95056842b49 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql @@ -0,0 +1,13 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +query getVariables { + ciVariables { + nodes { + ...BaseCiVariable + ... on CiInstanceVariable { + masked + protected + } + } + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js new file mode 100644 index 00000000000..7b57e97a4b8 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js @@ -0,0 +1,67 @@ +import axios from 'axios'; +import { + convertObjectPropsToCamelCase, + convertObjectPropsToSnakeCase, +} from '../../lib/utils/common_utils'; +import { getIdFromGraphQLId } from '../../graphql_shared/utils'; +import { instanceString } from '../constants'; +import getAdminVariables from './queries/variables.query.graphql'; + +const prepareVariableForApi = ({ variable, destroy = false }) => { + return { + ...convertObjectPropsToSnakeCase(variable), + id: getIdFromGraphQLId(variable?.id), + variable_type: variable.variableType.toLowerCase(), + secret_value: variable.value, + _destroy: destroy, + }; +}; + +const mapVariableTypes = (variables = [], kind) => { + return variables.map((ciVar) => { + return { + __typename: `Ci${kind}Variable`, + ...convertObjectPropsToCamelCase(ciVar), + variableType: ciVar.variable_type ? ciVar.variable_type.toUpperCase() : ciVar.variableType, + }; + }); +}; + +const prepareAdminGraphQLResponse = ({ data, errors = [] }) => { + return { + errors, + ciVariables: { + __typename: `Ci${instanceString}VariableConnection`, + nodes: mapVariableTypes(data.variables, instanceString), + }, + }; +}; + +const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) => { + try { + const { data } = await axios.patch(endpoint, { + variables_attributes: [prepareVariableForApi({ variable, destroy })], + }); + + return prepareAdminGraphQLResponse({ data }); + } catch (e) { + return prepareAdminGraphQLResponse({ + data: cache.readQuery({ query: getAdminVariables }), + errors: [...e.response.data], + }); + } +}; + +export const resolvers = { + Mutation: { + addAdminVariable: async (_, { endpoint, variable }, { cache }) => { + return callAdminEndpoint({ endpoint, variable, cache }); + }, + updateAdminVariable: async (_, { endpoint, variable }, { cache }) => { + return callAdminEndpoint({ endpoint, variable, cache }); + }, + deleteAdminVariable: async (_, { endpoint, variable }, { cache }) => { + return callAdminEndpoint({ endpoint, variable, cache, destroy: true }); + }, + }, +}; diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js index 2b54af6a2a4..713a453561e 100644 --- a/app/assets/javascripts/ci_variable_list/index.js +++ b/app/assets/javascripts/ci_variable_list/index.js @@ -2,8 +2,9 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; -import CiVariableSettings from './components/ci_variable_settings.vue'; +import CiAdminVariables from './components/ci_admin_variables.vue'; import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue'; +import { resolvers } from './graphql/resolvers'; import createStore from './store'; const mountCiVariableListApp = (containerEl) => { @@ -13,8 +14,12 @@ const mountCiVariableListApp = (containerEl) => { awsTipDeployLink, awsTipLearnLink, containsVariableReferenceLink, + endpoint, environmentScopeLink, - group, + groupId, + groupPath, + isGroup, + isProject, maskedEnvironmentVariablesLink, maskableRegex, projectFullPath, @@ -23,13 +28,16 @@ const mountCiVariableListApp = (containerEl) => { protectedEnvironmentVariablesLink, } = containerEl.dataset; - const isGroup = parseBoolean(group); + const parsedIsProject = parseBoolean(isProject); + const parsedIsGroup = parseBoolean(isGroup); const isProtectedByDefault = parseBoolean(protectedByDefault); + const component = CiAdminVariables; + Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient(resolvers), }); return new Vue({ @@ -41,8 +49,12 @@ const mountCiVariableListApp = (containerEl) => { awsTipDeployLink, awsTipLearnLink, containsVariableReferenceLink, + endpoint, environmentScopeLink, - isGroup, + groupId, + groupPath, + isGroup: parsedIsGroup, + isProject: parsedIsProject, isProtectedByDefault, maskedEnvironmentVariablesLink, maskableRegex, @@ -51,7 +63,7 @@ const mountCiVariableListApp = (containerEl) => { protectedEnvironmentVariablesLink, }, render(createElement) { - return createElement(CiVariableSettings); + return createElement(component); }, }); }; diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js index 009e56469d1..1faa97a5f73 100644 --- a/app/assets/javascripts/ci_variable_list/utils.js +++ b/app/assets/javascripts/ci_variable_list/utils.js @@ -2,20 +2,25 @@ import { uniq } from 'lodash'; import { allEnvironments } from './constants'; /** - * This function takes aa list of variable and environments + * This function takes a list of variable, environments and + * new environments added through the scope dropdown * and create a new Array that concatenate the environment list * with the environment scopes find in the variable list. This is * useful for variable settings so that we can render a list of all - * environment scopes available based on both the list of envs and what - * is found under each variable. + * environment scopes available based on the list of envs, the ones the user + * added explictly and what is found under each variable. * @param {Array} variables * @param {Array} environments * @returns {Array} - Array of environments */ -export const createJoinedEnvironments = (variables = [], environments = []) => { +export const createJoinedEnvironments = ( + variables = [], + environments = [], + newEnvironments = [], +) => { const scopesFromVariables = variables.map((variable) => variable.environmentScope); - return uniq(environments.concat(scopesFromVariables)).sort(); + return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort(); }; /** diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index 7c424088c8b..9cea89f4990 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -7,11 +7,12 @@ const DEFERRED_LINK_CLASS = 'deferred-link'; export default class PersistentUserCallout { constructor(container, options = container.dataset) { - const { dismissEndpoint, featureId, groupId, deferLinks } = options; + const { dismissEndpoint, featureId, groupId, namespaceId, deferLinks } = options; this.container = container; this.dismissEndpoint = dismissEndpoint; this.featureId = featureId; this.groupId = groupId; + this.namespaceId = namespaceId; this.deferLinks = parseBoolean(deferLinks); this.closeButtons = this.container.querySelectorAll('.js-close'); @@ -56,6 +57,7 @@ export default class PersistentUserCallout { .post(this.dismissEndpoint, { feature_name: this.featureId, group_id: this.groupId, + namespace_id: this.namespaceId, }) .then(() => { this.container.remove(); |