Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-06 00:09:04 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-06 00:09:04 +0300
commit96e23b2017cbe56969771960f6c274c5d3599397 (patch)
treeb8b17da1ab080dd41fc64fc0262de2cf16754559 /app
parent2f1a81fd16ff9968d6b986f8a407d963bc2218f9 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/markdown/utils.js27
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue108
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/index.js1
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue110
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql24
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/index.js36
-rw-r--r--app/assets/javascripts/design_management/components/design_description/description_form.vue234
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue12
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/update_design_description.mutation.graphql11
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql4
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue1
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js2
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/controllers/projects/environments_controller.rb58
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb57
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/projects/settings/operations_controller.rb12
-rw-r--r--app/models/diff_discussion.rb4
-rw-r--r--app/services/projects/operations/update_service.rb19
-rw-r--r--app/views/ci/group_variables/_index.html.haml31
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
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