diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-06 00:09:04 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-06 00:09:04 +0300 |
commit | 96e23b2017cbe56969771960f6c274c5d3599397 (patch) | |
tree | b8b17da1ab080dd41fc64fc0262de2cf16754559 /app | |
parent | 2f1a81fd16ff9968d6b986f8a407d963bc2218f9 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 576 insertions, 186 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/utils.js b/app/assets/javascripts/behaviors/markdown/utils.js new file mode 100644 index 00000000000..f02d6c0f813 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/utils.js @@ -0,0 +1,27 @@ +/** + * This method parses raw markdown text in GFM input field and toggles checkboxes + * based on checkboxChecked property. + * + * @param {Object} object containing rawMarkdown, sourcepos, checkboxChecked properties + * @returns String with toggled checkboxes + */ +export const toggleMarkCheckboxes = ({ rawMarkdown, sourcepos, checkboxChecked }) => { + // Extract the description text + const [startRange] = sourcepos.split('-'); + let [startRow] = startRange.split(':'); + startRow = Number(startRow) - 1; + + // Mark/Unmark the checkboxes + return rawMarkdown + .split('\n') + .map((row, index) => { + if (startRow === index) { + if (checkboxChecked) { + return row.replace(/\[ \]/, '[x]'); + } + return row.replace(/\[[x~]\]/i, '[ ]'); + } + return row; + }) + .join('\n'); +}; diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue index 6f6c55e07c7..3944d00b0e5 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue @@ -5,6 +5,7 @@ import { GlLoadingIcon, GlModalDirective, GlKeysetPagination, + GlLink, GlTable, GlTooltipDirective, } from '@gitlab/ui'; @@ -21,7 +22,7 @@ import { convertEnvironmentScope } from '../utils'; export default { modalId: ADD_CI_VARIABLE_MODAL_ID, - fields: [ + defaultFields: [ { key: 'variableType', label: s__('CiVariables|Type'), @@ -54,10 +55,34 @@ export default { thClass: 'gl-w-5p', }, ], + inheritedVarsFields: [ + { + key: 'variableType', + label: s__('CiVariables|Type'), + }, + { + key: 'key', + label: s__('CiVariables|Key'), + tdClass: 'text-plain', + }, + { + key: 'options', + label: s__('CiVariables|Options'), + }, + { + key: 'environmentScope', + label: s__('CiVariables|Environments'), + }, + { + key: 'group', + label: s__('CiVariables|Group'), + }, + ], components: { GlAlert, GlButton, GlKeysetPagination, + GlLink, GlLoadingIcon, GlTable, }, @@ -66,6 +91,7 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [glFeatureFlagsMixin()], + inject: ['isInheritedGroupVars'], props: { entity: { type: String, @@ -112,6 +138,9 @@ export default { showAlert() { return !this.isLoading && this.exceedsVariableLimit; }, + showPagination() { + return this.glFeatures.ciVariablesPages; + }, valuesButtonText() { return this.areValuesHidden ? __('Reveal values') : __('Hide values'); }, @@ -119,7 +148,12 @@ export default { return !this.variables || this.variables.length === 0; }, fields() { - return this.$options.fields; + return this.isInheritedGroupVars + ? this.$options.inheritedVarsFields + : this.$options.defaultFields; + }, + tableDataTestId() { + return this.isInheritedGroupVars ? 'inherited-ci-variable-table' : 'ci-variable-table'; }, variablesWithOptions() { return this.variables?.map((item, index) => ({ @@ -161,7 +195,7 @@ export default { </script> <template> - <div class="ci-variable-table" data-testid="ci-variable-table"> + <div class="ci-variable-table" :data-testid="tableDataTestId"> <gl-loading-icon v-if="isLoading" /> <gl-alert v-if="showAlert" @@ -172,7 +206,7 @@ export default { {{ exceedsVariableLimitText }} </gl-alert> <div - v-if="glFeatures.ciVariablesPages" + v-if="showPagination && !isInheritedGroupVars" class="ci-variable-actions gl-display-flex gl-justify-content-end gl-my-3" > <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button> @@ -231,7 +265,7 @@ export default { /> </div> </template> - <template #cell(value)="{ item }"> + <template v-if="!isInheritedGroupVars" #cell(value)="{ item }"> <div class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" > @@ -277,7 +311,21 @@ export default { /> </div> </template> - <template #cell(actions)="{ item }"> + <template v-if="isInheritedGroupVars" #cell(group)="{ item }"> + <div + class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" + > + <gl-link + :id="`ci-variable-group-${item.id}`" + data-testid="ci-variable-table-row-cicd-path" + class="gl-display-inline-block gl-max-w-full gl-word-break-word" + :href="item.groupCiCdSettingsPath" + > + {{ item.groupName }} + </gl-link> + </div> + </template> + <template v-if="!isInheritedGroupVars" #cell(actions)="{ item }"> <gl-button v-gl-modal-directive="$options.modalId" icon="pencil" @@ -300,28 +348,32 @@ export default { > {{ exceedsVariableLimitText }} </gl-alert> - <div v-if="!glFeatures.ciVariablesPages" 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" - :aria-label="__('Add')" - :disabled="exceedsVariableLimit" - @click="setSelectedVariable()" - >{{ __('Add variable') }}</gl-button - > - <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button> - </div> - <div v-else class="gl-display-flex gl-justify-content-center gl-mt-6"> - <gl-keyset-pagination - v-bind="pageInfo" - :prev-text="__('Previous')" - :next-text="__('Next')" - @prev="$emit('handle-prev-page')" - @next="$emit('handle-next-page')" - /> + <div v-if="!isInheritedGroupVars"> + <div v-if="!showPagination" 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" + :aria-label="__('Add')" + :disabled="exceedsVariableLimit" + @click="setSelectedVariable()" + >{{ __('Add variable') }}</gl-button + > + <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ + valuesButtonText + }}</gl-button> + </div> + <div v-else class="gl-display-flex gl-justify-content-center gl-mt-6"> + <gl-keyset-pagination + v-bind="pageInfo" + :prev-text="__('Previous')" + :next-text="__('Next')" + @prev="$emit('handle-prev-page')" + @next="$emit('handle-next-page')" + /> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/ci/ci_variable_list/index.js b/app/assets/javascripts/ci/ci_variable_list/index.js index 033cdbe864e..e47b41ceae5 100644 --- a/app/assets/javascripts/ci/ci_variable_list/index.js +++ b/app/assets/javascripts/ci/ci_variable_list/index.js @@ -67,6 +67,7 @@ const mountCiVariableListApp = (containerEl) => { groupId, groupPath, isGroup: parsedIsGroup, + isInheritedGroupVars: false, isProject: parsedIsProject, isProtectedByDefault, maskedEnvironmentVariablesLink, diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue new file mode 100644 index 00000000000..27ee1b794f6 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue @@ -0,0 +1,110 @@ +<script> +import { produce } from 'immer'; +import { s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { reportMessageToSentry } from '~/ci/ci_variable_list/utils'; +import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue'; +import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql'; + +export const i18n = { + fetchError: s__('CiVariables|There was an error fetching the inherited CI variables.'), + tooManyCallsError: s__( + 'CiVariables|Maximum number of Inherited Group CI variables loaded (2000)', + ), +}; + +export const VARIABLES_PER_FETCH = 100; +export const FETCH_LIMIT = 20; + +export default { + name: 'InheritedCiVariablesApp', + components: { + CiVariableTable, + }, + mixins: [glFeatureFlagsMixin()], + inject: ['projectPath'], + apollo: { + ciVariables: { + query: getInheritedCiVariables, + variables() { + return { + first: VARIABLES_PER_FETCH, + fullPath: this.projectPath, + }; + }, + update(data) { + return data.project.inheritedCiVariables?.nodes || []; + }, + result({ data }) { + this.pageInfo = data?.project?.inheritedCiVariables?.pageInfo || this.pageInfo; + this.hasNextPage = this.pageInfo?.hasNextPage || false; + if (!this.hasNextPage) { + return; + } + + // The query fetches 100 items at a time. + // Variables are batch loaded up to 20 consecutive API calls. + if (this.loadingCounter < FETCH_LIMIT) { + this.hasNextPage = false; + this.fetchMoreVariables(); + this.loadingCounter += 1; + } else { + createAlert({ message: this.$options.i18n.tooManyCallsError }); + reportMessageToSentry(this.$options.name, this.$options.i18n.tooManyCallsError, {}); + } + }, + error() { + this.showFetchError(); + }, + }, + }, + data() { + return { + ciVariables: [], + hasNextPage: false, + loadingCounter: 1, + pageInfo: {}, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.ciVariables.loading; + }, + }, + methods: { + fetchMoreVariables() { + this.$apollo.queries.ciVariables + .fetchMore({ + variables: { + after: this.pageInfo.endCursor, + }, + updateQuery(previousResult, { fetchMoreResult }) { + const previousVars = previousResult.project.inheritedCiVariables?.nodes; + const newVars = fetchMoreResult.project.inheritedCiVariables?.nodes; + + return produce(fetchMoreResult, (draftData) => { + draftData.project.inheritedCiVariables.nodes = previousVars.concat(newVars); + }); + }, + }) + .catch(this.showFetchError); + }, + showFetchError() { + this.hasNextPage = false; + createAlert({ message: this.$options.i18n.fetchError }); + }, + }, + i18n, +}; +</script> + +<template> + <ci-variable-table + entity="project" + :is-loading="isLoading" + :max-variable-limit="0" + :page-info="pageInfo" + :variables="ciVariables" + /> +</template> diff --git a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql new file mode 100644 index 00000000000..b25768632e1 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql @@ -0,0 +1,24 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getInheritedCiVariables($after: String, $first: Int, $fullPath: ID!) { + project(fullPath: $fullPath) { + id + inheritedCiVariables(after: $after, first: $first) { + pageInfo { + ...PageInfo + } + nodes { + __typename + id + key + variableType + environmentScope + groupCiCdSettingsPath + groupName + masked + protected + raw + } + } + } +} diff --git a/app/assets/javascripts/ci/inherited_ci_variables/index.js b/app/assets/javascripts/ci/inherited_ci_variables/index.js new file mode 100644 index 00000000000..324aae2a573 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { generateCacheConfig, resolvers } from '../ci_variable_list/graphql/settings'; +import InheritedCiVariables from './components/inherited_ci_variables_app.vue'; + +export default (containerId = 'js-inherited-group-ci-variables') => { + const el = document.getElementById(containerId); + + if (!el) { + return; + } + + const { projectPath } = el.dataset; + + Vue.use(VueApollo); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + resolvers, + generateCacheConfig(false), // set to true if we're using key-set pagination + ), + }); + + // eslint-disable-next-line consistent-return + return new Vue({ + el, + apolloProvider, + provide: { + isInheritedGroupVars: true, + projectPath, + }, + render(createElement) { + return createElement(InheritedCiVariables); + }, + }); +}; diff --git a/app/assets/javascripts/design_management/components/design_description/description_form.vue b/app/assets/javascripts/design_management/components/design_description/description_form.vue new file mode 100644 index 00000000000..890d7f80f8d --- /dev/null +++ b/app/assets/javascripts/design_management/components/design_description/description_form.vue @@ -0,0 +1,234 @@ +<script> +import { GlButton, GlFormGroup, GlAlert, GlTooltipDirective } from '@gitlab/ui'; + +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { __, s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; +import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import { toggleMarkCheckboxes } from '~/behaviors/markdown/utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import updateDesignDescriptionMutation from '../../graphql/mutations/update_design_description.mutation.graphql'; +import { UPDATE_DESCRIPTION_ERROR } from '../../utils/error_messages'; + +const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox'); + +export default { + components: { + MarkdownEditor, + GlAlert, + GlButton, + GlFormGroup, + }, + directives: { + SafeHtml, + GlTooltip: GlTooltipDirective, + }, + i18n: { + edit: __('Edit'), + editDescription: s__('DesignManagement|Edit description'), + descriptionLabel: s__('DesignManagement|Design description'), + }, + formFieldProps: { + id: 'design-description', + name: 'design-description', + placeholder: s__('DesignManagement|Write a comment or drag your files hereā¦'), + 'aria-label': s__('DesignManagement|Design description'), + }, + mixins: [glFeaturesFlagMixin()], + markdownDocsPath: helpPagePath('user/markdown'), + quickActionsDocsPath: helpPagePath('user/project/quick_actions'), + props: { + design: { + type: Object, + required: true, + }, + markdownPreviewPath: { + type: String, + required: true, + }, + designVariables: { + type: Object, + required: true, + }, + }, + data() { + return { + descriptionText: this.design.description || '', + showEditor: false, + isSubmitting: false, + errorMessage: '', + autosaveKey: `Issue/${getIdFromGraphQLId(this.design.issue.id)}/Design/${getIdFromGraphQLId( + this.design.id, + )}`, + }; + }, + computed: { + canUpdate() { + return this.design.issue?.userPermissions?.updateDesign && !this.showEditor; + }, + }, + watch: { + 'design.descriptionHtml': { + handler(newDescriptionHtml, oldDescriptionHtml) { + if (newDescriptionHtml !== oldDescriptionHtml) { + this.renderGFM(); + } + }, + immediate: true, + }, + }, + methods: { + startEditing() { + this.showEditor = true; + }, + closeForm() { + this.showEditor = false; + }, + async renderGFM() { + await this.$nextTick(); + renderGFM(this.$refs['gfm-content']); + + if (this.canUpdate) { + const checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox'); + + // enable boxes, disabled by default in markdown + checkboxes.forEach((checkbox) => { + // eslint-disable-next-line no-param-reassign + checkbox.disabled = false; + }); + } + }, + setDescriptionText(newText) { + // Do not update when cmd+enter is executed + if (!this.isSubmitting) { + this.descriptionText = newText; + } + }, + async updateDesignDescription() { + this.isSubmitting = true; + + try { + const designDescriptionInput = { description: this.descriptionText, id: this.design.id }; + + await this.$apollo.mutate({ + mutation: updateDesignDescriptionMutation, + variables: { + input: designDescriptionInput, + }, + }); + + this.closeForm(); + } catch { + this.errorMessage = UPDATE_DESCRIPTION_ERROR; + } finally { + this.isSubmitting = false; + } + }, + toggleCheckboxes(event) { + const { target } = event; + + if (isCheckbox(target)) { + target.disabled = true; + + const { sourcepos } = target.parentElement.dataset; + + if (!sourcepos) return; + + // Toggle checkboxes based on user input + this.descriptionText = toggleMarkCheckboxes({ + rawMarkdown: this.descriptionText, + checkboxChecked: target.checked, + sourcepos, + }); + + // Update the desciption text using mutation + this.updateDesignDescription(); + } + }, + }, +}; +</script> + +<template> + <div class="design-description-container"> + <gl-form-group + v-if="showEditor" + class="design-description-form common-note-form" + :label="$options.i18n.descriptionLabel" + > + <div v-if="errorMessage" class="gl-pb-3"> + <gl-alert variant="danger" @dismiss="errorMessage = null"> + {{ errorMessage }} + </gl-alert> + </div> + <markdown-editor + :value="descriptionText" + :render-markdown-path="markdownPreviewPath" + :markdown-docs-path="$options.markdownDocsPath" + :form-field-props="$options.formFieldProps" + :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)" + :quick-actions-docs-path="$options.quickActionsDocsPath" + :autosave-key="autosaveKey" + enable-autocomplete + :supports-quick-actions="false" + autofocus + @input="setDescriptionText" + @keydown.meta.enter="updateDesignDescription" + @keydown.ctrl.enter="updateDesignDescription" + @keydown.exact.esc.stop="closeForm" + /> + <div class="gl-display-flex gl-mt-3"> + <gl-button + category="primary" + variant="confirm" + :loading="isSubmitting" + data-testid="save-description" + @click="updateDesignDescription" + >{{ s__('DesignManagement|Save') }} + </gl-button> + <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="closeForm" + >{{ s__('DesignManagement|Cancel') }} + </gl-button> + </div> + </gl-form-group> + <div v-else class="design-description-view"> + <div + class="design-description-header gl-display-flex gl-justify-content-space-between gl-mb-2" + > + <label class="gl-m-0"> + {{ $options.i18n.descriptionLabel }} + </label> + <gl-button + v-if="canUpdate" + v-gl-tooltip + class="gl-ml-auto" + size="small" + data-testid="edit-description" + :aria-label="$options.i18n.editDescription" + @click="startEditing" + > + {{ $options.i18n.edit }} + </gl-button> + </div> + <div + v-if="!design.descriptionHtml" + data-testid="design-description-none" + class="gl-text-secondary gl-mb-5" + > + {{ s__('DesignManagement|None') }} + </div> + <div v-else class="design-description js-task-list-container"> + <div + ref="gfm-content" + v-safe-html="design.descriptionHtml" + class="md gl-mb-4" + data-testid="design-description-content" + @change="toggleCheckboxes" + ></div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue index c34d5cea0c2..9a8685f4c86 100644 --- a/app/assets/javascripts/design_management/components/design_sidebar.vue +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -9,6 +9,7 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; import { extractDiscussions, extractParticipants } from '../utils/design_management_utils'; import DesignDiscussion from './design_notes/design_discussion.vue'; +import DescriptionForm from './design_description/description_form.vue'; import DesignNoteSignedOut from './design_notes/design_note_signed_out.vue'; import DesignTodoButton from './design_todo_button.vue'; @@ -21,6 +22,7 @@ export default { GlAccordionItem, GlSkeletonLoader, DesignTodoButton, + DescriptionForm, }, mixins: [glFeatureFlagsMixin()], inject: { @@ -54,6 +56,10 @@ export default { type: Boolean, required: true, }, + designVariables: { + type: Object, + required: true, + }, }, data() { return { @@ -143,6 +149,12 @@ export default { :href="issue.webUrl" >{{ issue.webPath }}</a > + <description-form + v-if="!isLoading" + :design="design" + :design-variables="designVariables" + :markdown-preview-path="markdownPreviewPath" + /> <participants :participants="discussionParticipants" :show-participant-label="false" diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql index 9bd70e7e886..575201a7635 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql @@ -5,6 +5,8 @@ fragment DesignListItem on Design { notesCount image imageV432x230 + description + descriptionHtml currentUserTodos(state: pending) { nodes { id diff --git a/app/assets/javascripts/design_management/graphql/mutations/update_design_description.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/update_design_description.mutation.graphql new file mode 100644 index 00000000000..78b66477747 --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/mutations/update_design_description.mutation.graphql @@ -0,0 +1,11 @@ +mutation updateDesignDescriptionMutation($input: DesignManagementUpdateInput!) { + designManagementUpdate(input: $input) { + errors + design { + id + image + description + descriptionHtml + } + } +} diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql index 730467c33f6..c6eda2797d5 100644 --- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql +++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql @@ -25,6 +25,10 @@ query getDesign( ...Author } } + userPermissions { + createDesign + updateDesign + } } } } diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index eeb36e59b89..65e04b1ff98 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -385,6 +385,7 @@ export default { </div> <design-sidebar :design="design" + :design-variables="designVariables" :resolved-discussions-expanded="resolvedDiscussionsExpanded" :markdown-preview-path="markdownPreviewPath" :is-loading="isLoading" diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js index 7470f3d259b..2db34ea7103 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -61,6 +61,8 @@ export const designUploadOptimisticResponse = (files) => { id: -uniqueId(), image: '', imageV432x230: '', + description: '', + descriptionHtml: '', filename: file.name, fullPath: '', notesCount: 0, diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js index 1ed054abe22..2b5d04959b4 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -138,3 +138,7 @@ export const MAXIMUM_FILE_UPLOAD_LIMIT_REACHED = sprintf( upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, }, ); + +export const UPDATE_DESCRIPTION_ERROR = s__( + 'DesignManagement|Could not update description. Please try again.', +); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 731b1373987..b2681267e06 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -2,6 +2,7 @@ import initArtifactsSettings from '~/artifacts_settings'; import SecretValues from '~/behaviors/secret_values'; import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers'; import initVariableList from '~/ci/ci_variable_list'; +import initInheritedGroupCiVariables from '~/ci/inherited_ci_variables'; import initDeployFreeze from '~/deploy_freeze'; import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle'; import { initInstallRunner } from '~/pages/shared/mount_runner_instructions'; @@ -26,6 +27,7 @@ if (runnerToken) { } initVariableList(); +initInheritedGroupCiVariables(); // hide extra auto devops settings based checkbox state const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 0db26c544fa..ab7f9bb4927 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -1,22 +1,13 @@ # frozen_string_literal: true class Projects::EnvironmentsController < Projects::ApplicationController - # Metrics dashboard code is getting decoupled from environments and is being moved - # into app/controllers/projects/metrics_dashboard_controller.rb - # See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details. - MIN_SEARCH_LENGTH = 3 - include MetricsDashboard include ProductAnalyticsTracking include KasCookie layout 'project' - before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do - authorize_metrics_dashboard! - end - before_action only: [:show] do push_frontend_feature_flag(:environment_details_vue, @project) end @@ -29,12 +20,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_settings_to_graphql, @project) end - before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect] + before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_stop_environment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update, :cancel_auto_stop] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] - before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :cancel_auto_stop] before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? } before_action :set_kas_cookie, only: [:index], if: -> { current_user } @@ -179,41 +170,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end - def metrics_redirect - return not_found if Feature.enabled?(:remove_monitor_metrics) - - redirect_to project_metrics_dashboard_path(project) - end - - def metrics - return not_found if Feature.enabled?(:remove_monitor_metrics) - - respond_to do |format| - format.html do - redirect_to project_metrics_dashboard_path(project, environment: environment) - end - format.json do - # Currently, this acts as a hint to load the metrics details into the cache - # if they aren't there already - @metrics = environment.metrics || {} - - render json: @metrics, status: @metrics.any? ? :ok : :no_content - end - end - end - - def additional_metrics - return not_found if Feature.enabled?(:remove_monitor_metrics) - - respond_to do |format| - format.json do - additional_metrics = environment.additional_metrics(*metrics_params) || {} - - render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content - end - end - end - def search respond_to do |format| format.json do @@ -265,16 +221,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController @search_environments ||= Environments::EnvironmentsFinder.new(project, current_user, type: type, search: search).execute end - def metrics_params - params.require([:start, :end]) - end - - def metrics_dashboard_params - params - .permit(:embedded, :group, :title, :y_label, :dashboard_path, :environment, :sample_metrics, :embed_json) - .merge(dashboard_path: params[:dashboard], environment: environment) - end - def include_all_dashboards? !params[:embedded] end diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb deleted file mode 100644 index c95594d87c0..00000000000 --- a/app/controllers/projects/metrics_dashboard_controller.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true -module Projects - class MetricsDashboardController < Projects::ApplicationController - # Metrics dashboard code is in the process of being decoupled from environments - # and is getting moved to this controller. Some code may be duplicated from - # app/controllers/projects/environments_controller.rb - # See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details. - - include Gitlab::Utils::StrongMemoize - - before_action :authorize_metrics_dashboard! - before_action :render_404, only: :show, if: -> do - Feature.enabled?(:remove_monitor_metrics) - end - - feature_category :metrics - urgency :low - - def show - return not_found if Feature.enabled?(:remove_monitor_metrics) - - if environment - render 'projects/environments/metrics' - elsif default_environment - redirect_to project_metrics_dashboard_path( - project, - # Reverse merge the query parameters so that a query parameter named dashboard_path doesn't - # override the dashboard_path path parameter. - **permitted_params.to_h.symbolize_keys - .merge(environment: default_environment.id) - .reverse_merge(request.query_parameters.symbolize_keys) - ) - else - render 'projects/environments/empty_metrics' - end - end - - private - - def permitted_params - @permitted_params ||= params.permit(:dashboard_path, :environment, :page) - end - - def environment - strong_memoize(:environment) do - env = permitted_params[:environment] - project.environments.find(env) if env - end - end - - def default_environment - strong_memoize(:default_environment) do - project.default_environment - end - end - end -end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index ce760051f79..aa1c7069e2b 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -18,6 +18,7 @@ module Projects push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace) push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project) push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project) + push_frontend_feature_flag(:ci_vueify_inherited_group_variables, @project) end helper_method :highlight_badge diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb index 952c9e90a2c..5f30edd3db8 100644 --- a/app/controllers/projects/settings/operations_controller.rb +++ b/app/controllers/projects/settings/operations_controller.rb @@ -122,22 +122,14 @@ module Projects { incident_management_setting_attributes: ::Gitlab::Tracking::IncidentManagement.tracking_keys.keys, - metrics_setting_attributes: [:external_dashboard_url, :dashboard_timezone], - error_tracking_setting_attributes: [ :enabled, :integrated, :api_host, :token, project: [:slug, :name, :organization_slug, :organization_name, :sentry_project_id] - ], - - grafana_integration_attributes: [:token, :grafana_url, :enabled] - }.tap do |potential_params| - if Feature.enabled?(:remove_monitor_metrics) - potential_params.except!(:metrics_setting_attributes, :grafana_integration_attributes) - end - end + ] + } end end end diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index e2ee951522d..c4ccb9ef4f5 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -37,8 +37,8 @@ class DiffDiscussion < Discussion def reply_attributes super.merge( - original_position: Gitlab::Json.dump(original_position), - position: Gitlab::Json.dump(position) + original_position: Gitlab::Json.dump(original_position.to_h), + position: Gitlab::Json.dump(position.to_h) ) end diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index d0bef9da329..e7a8d5305ea 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -14,8 +14,6 @@ module Projects def project_update_params error_tracking_params .merge(alerting_setting_params) - .merge(metrics_setting_params) - .merge(grafana_integration_params) .merge(prometheus_integration_params) .merge(incident_management_setting_params) end @@ -37,15 +35,6 @@ module Projects { alerting_setting_attributes: attr } end - def metrics_setting_params - attribs = params[:metrics_setting_attributes] - return {} unless attribs - - attribs[:external_dashboard_url] = attribs[:external_dashboard_url].presence - - { metrics_setting_attributes: attribs } - end - def error_tracking_params settings = params[:error_tracking_setting_attributes] return {} if settings.blank? @@ -99,14 +88,6 @@ module Projects params end - def grafana_integration_params - return {} unless attrs = params[:grafana_integration_attributes] - - destroy = attrs[:grafana_url].blank? && attrs[:token].blank? - - { grafana_integration_attributes: attrs.merge(_destroy: destroy) } - end - def prometheus_integration_params return {} unless attrs = params[:prometheus_integration_attributes] diff --git a/app/views/ci/group_variables/_index.html.haml b/app/views/ci/group_variables/_index.html.haml index c8c970f3c2f..538bbc486f3 100644 --- a/app/views/ci/group_variables/_index.html.haml +++ b/app/views/ci/group_variables/_index.html.haml @@ -1,14 +1,21 @@ - variables = @project.group.self_and_ancestors.flat_map(&:variables) -.ci-variable-table - %table.gl-table.gl-w-full.gl-table-layout-fixed - = render 'ci/group_variables/variable_header' - - variables.each do |variable| - %tr - %td.gl-text-truncate - = variable.key - %td.gl-text-truncate - = variable.environment_scope - %td.gl-text-truncate - %a.group-origin-link{ href: group_settings_ci_cd_path(variable.group) } - = variable.group.name +- if Feature.enabled?(:ci_vueify_inherited_group_variables) + #js-inherited-group-ci-variables{ + data: { + project_path: @project.full_path, + } + } +- else + .inherited-ci-variable-table + %table.gl-table.gl-w-full.gl-table-layout-fixed + = render 'ci/group_variables/variable_header' + - variables.each do |variable| + %tr + %td.gl-text-truncate + = variable.key + %td.gl-text-truncate + = variable.environment_scope + %td.gl-text-truncate + %a.group-origin-link{ href: group_settings_ci_cd_path(variable.group) } + = variable.group.name diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 6fa76297679..8fec5600780 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -30,8 +30,6 @@ help_text: s_('GroupSettings|Group members are not notified if the group is mentioned.') = render 'groups/settings/resource_access_token_creation', f: f, group: @group - - unless Feature.enabled?(:always_perform_delayed_deletion) - = render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group = render 'groups/settings/ip_restriction_registration_features_cta', f: f = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group |