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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob_edit/constants.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js71
-rw-r--r--app/assets/javascripts/editor/editor_lite.js20
-rw-r--r--app/assets/javascripts/gl_form.js6
-rw-r--r--app/assets/javascripts/issue_show/components/pinned_links.vue65
-rw-r--r--app/assets/javascripts/issue_show/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue20
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/typedefs.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue31
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss21
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/models/plan.rb2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml5
-rw-r--r--app/views/projects/blob/new.html.haml6
-rw-r--r--changelogs/unreleased/217362-restructure-usage-ping-add-usage-activity-to-ce.yml5
-rw-r--r--changelogs/unreleased/220935.yml5
-rw-r--r--changelogs/unreleased/fix-update-plan-limits-functionality.yml5
-rw-r--r--changelogs/unreleased/services-usage-5.yml5
-rw-r--r--changelogs/unreleased/services-usage-6.yml5
-rw-r--r--doc/administration/instance_limits.md14
-rw-r--r--doc/api/resource_milestone_events.md4
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/approval_rules.md280
-rw-r--r--doc/user/discussions/index.md26
-rw-r--r--doc/user/gitlab_com/index.md14
-rw-r--r--doc/user/project/repository/index.md3
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/gitlab/usage_data.rb94
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json4
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb733
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb23
-rw-r--r--spec/features/projects/blobs/edit_spec.rb12
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb3
-rw-r--r--spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb24
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb14
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb18
-rw-r--r--spec/frontend/issue_show/components/pinned_links_spec.js15
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb76
-rw-r--r--spec/models/plan_spec.rb12
-rw-r--r--yarn.lock16
46 files changed, 1290 insertions, 430 deletions
diff --git a/app/assets/javascripts/blob_edit/constants.js b/app/assets/javascripts/blob_edit/constants.js
new file mode 100644
index 00000000000..a19da2098cf
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/constants.js
@@ -0,0 +1,4 @@
+import { __ } from '~/locale';
+
+export const BLOB_EDITOR_ERROR = __('An error occurred while rendering the editor');
+export const BLOB_PREVIEW_ERROR = __('An error occurred previewing the blob');
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 011898a5e7a..a725b3fe5d6 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -3,39 +3,75 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
-import { __ } from '~/locale';
+import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
+const monacoEnabled = window?.gon?.features?.monacoBlobs;
+
export default class EditBlob {
// The options object has:
// assetsPath, filePath, currentAction, projectId, isMarkdown
constructor(options) {
this.options = options;
- this.configureAceEditor();
- this.initModePanesAndLinks();
- this.initSoftWrap();
- this.initFileSelectors();
+ const { isMarkdown } = this.options;
+ Promise.resolve()
+ .then(() => {
+ return monacoEnabled ? this.configureMonacoEditor() : this.configureAceEditor();
+ })
+ .then(() => {
+ this.initModePanesAndLinks();
+ this.initFileSelectors();
+ this.initSoftWrap();
+ if (isMarkdown) {
+ addEditorMarkdownListeners(this.editor);
+ }
+ this.editor.focus();
+ })
+ .catch(() => createFlash(BLOB_EDITOR_ERROR));
+ }
+
+ configureMonacoEditor() {
+ return import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite').then(
+ EditorModule => {
+ const EditorLite = EditorModule.default;
+ const editorEl = document.getElementById('editor');
+ const fileNameEl =
+ document.getElementById('file_path') || document.getElementById('file_name');
+ const fileContentEl = document.getElementById('file-content');
+ const form = document.querySelector('.js-edit-blob-form');
+
+ this.editor = new EditorLite();
+
+ this.editor.createInstance({
+ el: editorEl,
+ blobPath: fileNameEl.value,
+ blobContent: editorEl.innerText,
+ });
+
+ fileNameEl.addEventListener('change', () => {
+ this.editor.updateModelLanguage(fileNameEl.value);
+ });
+
+ form.addEventListener('submit', () => {
+ fileContentEl.value = this.editor.getValue();
+ });
+ },
+ );
}
configureAceEditor() {
- const { filePath, assetsPath, isMarkdown } = this.options;
+ const { filePath, assetsPath } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor');
- if (isMarkdown) {
- addEditorMarkdownListeners(this.editor);
- }
-
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
- this.editor.focus();
-
if (filePath) {
this.editor.getSession().setMode(getModeByFileExtension(filePath));
}
@@ -81,7 +117,7 @@ export default class EditBlob {
currentPane.empty().append(data);
currentPane.renderGFM();
})
- .catch(() => createFlash(__('An error occurred previewing the blob')));
+ .catch(() => createFlash(BLOB_PREVIEW_ERROR));
}
this.$toggleButton.show();
@@ -90,14 +126,19 @@ export default class EditBlob {
}
initSoftWrap() {
- this.isSoftWrapped = false;
+ this.isSoftWrapped = Boolean(monacoEnabled);
this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.$toggleButton.on('click', () => this.toggleSoftWrap());
}
toggleSoftWrap() {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
- this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ if (monacoEnabled) {
+ this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' });
+ } else {
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ }
}
}
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 020ed6dc867..6db24a1b7ef 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,4 +1,4 @@
-import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
@@ -70,6 +70,22 @@ export default class Editor {
}
getValue() {
- return this.model.getValue();
+ return this.instance.getValue();
+ }
+
+ setValue(val) {
+ this.instance.setValue(val);
+ }
+
+ focus() {
+ this.instance.focus();
+ }
+
+ navigateFileStart() {
+ this.instance.setPosition(new Position(1, 1));
+ }
+
+ updateOptions(options = {}) {
+ this.instance.updateOptions(options);
}
}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 0b7735a7db9..28f1e3afd3d 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -9,13 +9,15 @@ export default class GLForm {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
+
// Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
- if (item !== 'emojis') {
- this.enableGFM[item] = Boolean(dataSources[item]);
+ if (item !== 'emojis' && !dataSources[item]) {
+ this.enableGFM[item] = false;
}
});
+
// Before we start, we should clean up any previous data for this form
this.destroy();
// Set up the form
diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue
index 4b50acceb62..a877aa2ac96 100644
--- a/app/assets/javascripts/issue_show/components/pinned_links.vue
+++ b/app/assets/javascripts/issue_show/components/pinned_links.vue
@@ -1,11 +1,10 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton } from '@gitlab/ui';
+import { STATUS_PAGE_PUBLISHED, JOIN_ZOOM_MEETING } from '../constants';
export default {
components: {
- Icon,
- GlLink,
+ GlButton,
},
props: {
zoomMeetingUrl: {
@@ -19,32 +18,46 @@ export default {
default: '',
},
},
+ computed: {
+ pinnedLinks() {
+ return [
+ {
+ id: 'publishedIncidentUrl',
+ url: this.publishedIncidentUrl,
+ text: STATUS_PAGE_PUBLISHED,
+ icon: 'tanuki',
+ },
+ {
+ id: 'zoomMeetingUrl',
+ url: this.zoomMeetingUrl,
+ text: JOIN_ZOOM_MEETING,
+ icon: 'brand-zoom',
+ },
+ ];
+ },
+ },
+ methods: {
+ needsPaddingClass(i) {
+ return i < this.pinnedLinks.length - 1;
+ },
+ },
};
</script>
<template>
<div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
- <div v-if="publishedIncidentUrl" class="gl-pr-3">
- <gl-link
- :href="publishedIncidentUrl"
- target="_blank"
- class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
- data-testid="publishedIncidentUrl"
- >
- <icon name="tanuki" :size="14" />
- <strong class="vertical-align-top">{{ __('Published on status page') }}</strong>
- </gl-link>
- </div>
- <div v-if="zoomMeetingUrl">
- <gl-link
- :href="zoomMeetingUrl"
- target="_blank"
- class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
- data-testid="zoomMeetingUrl"
- >
- <icon name="brand-zoom" :size="14" />
- <strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong>
- </gl-link>
- </div>
+ <template v-for="(link, i) in pinnedLinks">
+ <div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
+ <gl-button
+ :href="link.url"
+ target="_blank"
+ :icon="link.icon"
+ size="small"
+ class="gl-font-weight-bold gl-mb-5"
+ :data-testid="link.id"
+ >{{ link.text }}</gl-button
+ >
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js
index d73cc8cf007..6bc6ed2b372 100644
--- a/app/assets/javascripts/issue_show/constants.js
+++ b/app/assets/javascripts/issue_show/constants.js
@@ -15,3 +15,6 @@ export const IssuableType = {
Epic: 'epic',
MergeRequest: 'merge_request',
};
+
+export const STATUS_PAGE_PUBLISHED = __('Published on status page');
+export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index be7f27f210d..fe5c289152d 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import store from '~/pipelines/stores/test_reports';
import { __ } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
@@ -11,6 +12,9 @@ export default {
Icon,
SmartVirtualList,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
store,
props: {
heading: {
@@ -69,12 +73,24 @@ export default {
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div>
- <div class="table-mobile-content pr-md-1 text-truncate">{{ testCase.classname }}</div>
+ <div
+ v-gl-tooltip
+ :title="testCase.classname"
+ class="table-mobile-content pr-md-1 text-truncate"
+ >
+ {{ testCase.classname }}
+ </div>
</div>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div class="table-mobile-content pr-md-1 text-truncate">{{ testCase.name }}</div>
+ <div
+ v-gl-tooltip
+ :title="testCase.name"
+ class="table-mobile-content pr-md-1 text-truncate"
+ >
+ {{ testCase.name }}
+ </div>
</div>
<div class="table-section section-10 section-wrap">
diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
index 59da2e27144..78cc1746cdb 100644
--- a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
@@ -22,7 +22,7 @@ type AppData {
username: String!
}
-type SubmitContentChangesInput {
+input SubmitContentChangesInput {
project: String!
sourcePath: String!
content: String!
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index a7fba5e760b..0ef4f1eda27 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -3,18 +3,19 @@ import { escape } from 'lodash';
import Tribute from 'tributejs';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
/**
* Creates the HTML template for each row of the mentions dropdown.
*
- * @param original An object from the array returned from the `autocomplete_sources/members` API
- * @returns {string} An HTML template
+ * @param original - An object from the array returned from the `autocomplete_sources/members` API
+ * @returns {string} - An HTML template
*/
function menuItemTemplate({ original }) {
const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
- gl-display-inline-flex gl-align-items-center gl-justify-content-center`;
+ gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
const avatarTag = original.avatar_url
? `<img
@@ -48,6 +49,7 @@ export default {
},
data() {
return {
+ assignees: undefined,
members: undefined,
};
},
@@ -76,19 +78,37 @@ export default {
*/
getMembers(inputText, processValues) {
if (this.members) {
- processValues(this.members);
+ processValues(this.getFilteredMembers());
} else if (this.dataSources.members) {
axios
.get(this.dataSources.members)
.then(response => {
this.members = response.data;
- processValues(response.data);
+ processValues(this.getFilteredMembers());
})
.catch(() => {});
} else {
processValues([]);
}
},
+ getFilteredMembers() {
+ const fullText = this.$slots.default[0].elm.value;
+
+ if (!this.assignees) {
+ this.assignees =
+ SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
+ }
+
+ if (fullText.startsWith('/assign @')) {
+ return this.members.filter(member => !this.assignees.includes(member.username));
+ }
+
+ if (fullText.startsWith('/unassign @')) {
+ return this.members.filter(member => this.assignees.includes(member.username));
+ }
+
+ return this.members;
+ },
},
render(createElement) {
return createElement('div', this.$slots.default);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 0e05f4a4622..89844f07e7e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -4,21 +4,25 @@ import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
-import Flash from '../../../flash';
-import GLForm from '../../../gl_form';
-import markdownHeader from './header.vue';
-import markdownToolbar from './toolbar.vue';
-import icon from '../icon.vue';
+import Flash from '~/flash';
+import GLForm from '~/gl_form';
+import MarkdownHeader from './header.vue';
+import MarkdownToolbar from './toolbar.vue';
+import Icon from '../icon.vue';
+import GlMentions from '~/vue_shared/components/gl_mentions.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
- markdownHeader,
- markdownToolbar,
- icon,
+ GlMentions,
+ MarkdownHeader,
+ MarkdownToolbar,
+ Icon,
Suggestions,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
isSubmitting: {
type: Boolean,
@@ -159,12 +163,10 @@ export default {
},
},
mounted() {
- /*
- GLForm class handles all the toolbar buttons
- */
+ // GLForm class handles all the toolbar buttons
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
- members: this.enableAutocomplete,
+ members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
@@ -243,7 +245,10 @@ export default {
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
- <slot name="textarea"></slot>
+ <gl-mentions v-if="glFeatures.tributeAutocomplete">
+ <slot name="textarea"></slot>
+ </gl-mentions>
+ <slot v-else name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
href="#"
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 8d5afe1d312..c1ca4a6f3d1 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -74,19 +74,6 @@
}
}
- &:focus:hover,
- &:focus {
- &.header-user-dropdown-toggle .header-user-notification-dot {
- border-color: $white;
- }
- }
-
- &:hover {
- &.header-user-dropdown-toggle .header-user-notification-dot {
- border-color: $nav-svg-color + 33;
- }
- }
-
&:hover,
&:focus {
@include media-breakpoint-up(sm) {
@@ -96,6 +83,10 @@
svg {
fill: currentColor;
}
+
+ &.header-user-dropdown-toggle .header-user-notification-dot {
+ border-color: $nav-svg-color + 33;
+ }
}
}
@@ -109,6 +100,10 @@
fill: $nav-svg-color;
}
}
+
+ &.header-user-dropdown-toggle .header-user-notification-dot {
+ border-color: $white;
+ }
}
.impersonated-user,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 693329848de..72fea21b52b 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
+ push_frontend_feature_flag(:tribute_autocomplete, @project)
end
before_action only: :show do
diff --git a/app/models/plan.rb b/app/models/plan.rb
index acac5f9aeae..b4091e0a755 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -27,7 +27,7 @@ class Plan < ApplicationRecord
end
def actual_limits
- self.limits || PlanLimits.new
+ self.limits || self.build_limits
end
def default?
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 032df24a603..67c78620f15 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -40,7 +40,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
- %pre.js-edit-mode-pane.qa-editor#editor= params[:content] || local_assigns[:blob_data]
+ %pre.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 870e37488cf..3d84adbc49a 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,7 +1,8 @@
- breadcrumb_title "Repository"
- page_title "Edit", @blob.path, @ref
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
+- unless Feature.enabled?(:monaco_blobs)
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
- if @conflict
.alert.alert-danger
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 8f166e9aa16..f9abcffeeb4 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,9 @@
- breadcrumb_title "Repository"
- page_title "New File", @path.presence, @ref
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
+- unless Feature.enabled?(:monaco_blobs)
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+
.editor-title-row
%h3.page-title.blob-new-page-title
New file
diff --git a/changelogs/unreleased/217362-restructure-usage-ping-add-usage-activity-to-ce.yml b/changelogs/unreleased/217362-restructure-usage-ping-add-usage-activity-to-ce.yml
new file mode 100644
index 00000000000..45e767c13e6
--- /dev/null
+++ b/changelogs/unreleased/217362-restructure-usage-ping-add-usage-activity-to-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Move Usage activity by stage for Configure to Core
+merge_request: 33672
+author:
+type: changed
diff --git a/changelogs/unreleased/220935.yml b/changelogs/unreleased/220935.yml
new file mode 100644
index 00000000000..723b2d6ae3e
--- /dev/null
+++ b/changelogs/unreleased/220935.yml
@@ -0,0 +1,5 @@
+---
+title: Update pinned links to use GlButton
+merge_request: 34620
+author:
+type: other
diff --git a/changelogs/unreleased/fix-update-plan-limits-functionality.yml b/changelogs/unreleased/fix-update-plan-limits-functionality.yml
new file mode 100644
index 00000000000..ae3769f6dcf
--- /dev/null
+++ b/changelogs/unreleased/fix-update-plan-limits-functionality.yml
@@ -0,0 +1,5 @@
+---
+title: Assign plan_id when building a new plan limit
+merge_request: 34845
+author:
+type: fixed
diff --git a/changelogs/unreleased/services-usage-5.yml b/changelogs/unreleased/services-usage-5.yml
new file mode 100644
index 00000000000..04657cf147d
--- /dev/null
+++ b/changelogs/unreleased/services-usage-5.yml
@@ -0,0 +1,5 @@
+---
+title: Use GpgKeys::CreateService when an admin creates a new GPG key for a user
+merge_request: 34737
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/services-usage-6.yml b/changelogs/unreleased/services-usage-6.yml
new file mode 100644
index 00000000000..14e4384ad94
--- /dev/null
+++ b/changelogs/unreleased/services-usage-6.yml
@@ -0,0 +1,5 @@
+---
+title: Use GpgKeys::CreateService when a user creates GPG keys for themselves via the API
+merge_request: 34817
+author: Rajendra Kadam
+type: fixed
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index db2cd7b477a..f466c721416 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -77,10 +77,10 @@ To set this limit on a self-managed installation, run the following in the
# Plan.default.create_limits!
# For project webhooks
-Plan.default.limits.update!(project_hooks: 100)
+Plan.default.actual_limits.update!(project_hooks: 100)
# For group webhooks
-Plan.default.limits.update!(group_hooks: 100)
+Plan.default.actual_limits.update!(group_hooks: 100)
```
NOTE: **Note:** Set the limit to `0` to disable it.
@@ -115,7 +115,7 @@ To set this limit on a self-managed installation, run the following in the
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
-Plan.default.limits.update!(offset_pagination_limit: 10000)
+Plan.default.actual_limits.update!(offset_pagination_limit: 10000)
```
- **Default offset pagination limit:** 50000
@@ -149,7 +149,7 @@ To set this limit on a self-managed installation, run the following in the
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
-Plan.default.limits.update!(ci_active_jobs: 500)
+Plan.default.actual_limits.update!(ci_active_jobs: 500)
```
NOTE: **Note:** Set the limit to `0` to disable it.
@@ -171,7 +171,7 @@ To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
-Plan.default.limits.update!(ci_project_subscriptions: 500)
+Plan.default.actual_limits.update!(ci_project_subscriptions: 500)
```
NOTE: **Note:** Set the limit to `0` to disable it.
@@ -196,7 +196,7 @@ To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
-Plan.default.limits.update!(ci_pipeline_schedules: 100)
+Plan.default.actual_limits.update!(ci_pipeline_schedules: 100)
```
### Number of instance level variables
@@ -214,7 +214,7 @@ To update this limit to a new value on a self-managed installation, run the foll
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
-Plan.default.limits.update!(ci_instance_level_variables: 30)
+Plan.default.actual_limits.update!(ci_instance_level_variables: 30)
```
## Instance monitoring and metrics
diff --git a/doc/api/resource_milestone_events.md b/doc/api/resource_milestone_events.md
index 695687ada6d..8a81615857c 100644
--- a/doc/api/resource_milestone_events.md
+++ b/doc/api/resource_milestone_events.md
@@ -6,8 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Resource milestone events API
-Resource milestone events keep track of what happens to GitLab [issues](../user/project/issues/),
-[merge requests](../user/project/merge_requests/), and [epics](../user/group/epics/).
+Resource milestone events keep track of what happens to GitLab [issues](../user/project/issues/) and
+[merge requests](../user/project/merge_requests/).
Use them to track which milestone was added or removed, who did it, and when it happened.
diff --git a/doc/development/README.md b/doc/development/README.md
index d77b5d3eea4..88abc638ac6 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -100,6 +100,7 @@ Complementary reads:
- [Renaming features](renaming_features.md)
- [Windows Development on GCP](windows.md)
- [Code Intelligence](code_intelligence/index.md)
+- [Approval Rules](approval_rules.md)
## Performance guides
diff --git a/doc/development/approval_rules.md b/doc/development/approval_rules.md
new file mode 100644
index 00000000000..65df82721de
--- /dev/null
+++ b/doc/development/approval_rules.md
@@ -0,0 +1,280 @@
+# Approval Rules **(STARTER)**
+
+This document explains the backend design and flow of all related functionality
+about [merge request approval rules](../user/project/merge_requests/merge_request_approvals.md).
+
+This should help contributors to understand the code design easier and to also
+help see if there are parts to improve as the feature and its implementation
+evolves.
+
+It's intentional that it doesn't contain too much implementation detail as they
+can change often. The code should explain those things better. The components
+mentioned here are the major parts of the application for the approval rules
+feature to work.
+
+NOTE: **Note:**
+This is a living document and should be updated accordingly when parts
+of the codebase touched in this document changed/removed or when new components
+are added.
+
+## Data Model
+
+```mermaid
+erDiagram
+ Project ||--o{ MergeRequest: " "
+ Project ||--o{ ApprovalProjectRule: " "
+ ApprovalProjectRule }o--o{ User: " "
+ ApprovalProjectRule }o--o{ Group: " "
+ ApprovalProjectRule }o--o{ ProtectedBranch: " "
+ MergeRequest ||--|| ApprovalState: " "
+ ApprovalState ||--o{ ApprovalWrappedRule: " "
+ MergeRequest ||--o{ Approval: " "
+ MergeRequest ||--o{ ApprovalMergeRequestRule: " "
+ ApprovalMergeRequestRule }o--o{ User: " "
+ ApprovalMergeRequestRule }o--o{ Group: " "
+ ApprovalMergeRequestRule ||--o| ApprovalProjectRule: " "
+```
+
+### `Project` and `MergeRequest`
+
+`Project` and `MergeRequest` models are defined in `ee/app/models/ee/project.rb`
+and `ee/app/models/ee/merge_request.rb`. They extend the non-EE versions since
+approval rules is an EE only feature. Associations and other related stuff to
+merge request approvals are defined here.
+
+### `ApprovalState`
+
+```mermaid
+erDiagram
+ MergeRequest ||--|| ApprovalState: " "
+```
+
+`ApprovalState` class is defined in `ee/app/models/approval_state.rb`. It's not
+an actual `ActiveRecord` model. This class encapsulates all logic related to the
+state of the approvals for a certain merge request like:
+
+- Knowing the approval rules that are applicable to the merge request based on
+ its target branch.
+- Knowing the approval rules that are applicable to a certain target branch.
+- Checking if all rules were approved.
+- Checking if approval is required.
+- Knowing how many approvals were given or still required.
+
+It gets the approval rules data from the project (`ApprovalProjectRule`) or the
+merge request (`ApprovalMergeRequestRule`) and wrap it as `ApprovalWrappedRule`.
+
+### `ApprovalProjectRule`
+
+```mermaid
+erDiagram
+ Project ||--o{ ApprovalProjectRule: " "
+ ApprovalProjectRule }o--o{ User: " "
+ ApprovalProjectRule }o--o{ Group: " "
+ ApprovalProjectRule }o--o{ ProtectedBranch: " "
+```
+
+`ApprovalProjectRule` model is defined in `ee/app/models/approval_project_rule.rb`.
+
+A record is created/updated/deleted when an approval rule is added/edited/removed
+via project settings or the [project level approvals API](../api/merge_request_approvals.md#project-level-mr-approvals).
+The `ApprovalState` model get these records when approval rules are not
+overwritten.
+
+The `protected_branches` attribute is set and used when a rule is scoped to
+protected branches. See [Scoped to Protected Branch doc](../user/project/merge_requests/merge_request_approvals.md#scoped-to-protected-branch-premium)
+for more information about the feature.
+
+### `ApprovalMergeRequestRule`
+
+```mermaid
+erDiagram
+ MergeRequest ||--o{ ApprovalMergeRequestRule: " "
+ ApprovalMergeRequestRule }o--o{ User: " "
+ ApprovalMergeRequestRule }o--o{ Group: " "
+ ApprovalMergeRequestRule ||--o| ApprovalProjectRule: " "
+```
+
+`ApprovalMergeRequestRule` model is defined in `ee/app/models/approval_merge_request_rule.rb`.
+
+A record is created/updated/deleted when a rule is added/edited/removed via merge
+request create/edit form or the [merge request level approvals API](../api/merge_request_approvals.md#merge-request-level-mr-approvals).
+
+The `approval_project_rule` is set when it is based from an existing `ApprovalProjectRule`.
+
+An `ApprovalMergeRequestRule` doesn't have `protected_branches` as it inherits
+them from the `approval_project_rule` if not overridden.
+
+### `ApprovalWrappedRule`
+
+```mermaid
+erDiagram
+ ApprovalState ||--o{ ApprovalWrappedRule: " "
+```
+
+`ApprovalWrappedRule` is defined in `ee/app/modes/approval_wrapped_rule.rb` and
+is not an `ActiveRecord` model. It's used to wrap an `ApprovalProjectRule` or
+`ApprovalMergeRequestRule` for common interface. It also has the following sub
+types:
+
+- `ApprovalWrappedAnyApprovalRule` - for wrapping an `any_approver` rule.
+- `ApprovalWrappedCodeOwnerRule` - for wrapping a `code_owner` rule.
+
+This class delegates most of the responsibilities to the approval rule it wraps
+but it's also responsible for:
+
+- Checking if the approval rule is approved.
+- Knowing how many approvals were given or still required for the approval rule.
+
+It gets this information from the approval rule and the `Approval` records from
+the merge request.
+
+### `Approval`
+
+```mermaid
+erDiagram
+ MergeRequest ||--o{ Approval: " "
+```
+
+`Approval` model is defined in `ee/app/models/approval.rb`. This model is
+responsible for storing information about an approval made on a merge request.
+Whenever an approval is given/revoked, a record is created/deleted.
+
+## Controllers and Services
+
+The following controllers and services below are being utilized for the approval
+rules feature to work.
+
+### `API::ProjectApprovalSettings`
+
+This private API is defined in `ee/lib/api/project_approval_settings.rb`.
+
+This is used for the following:
+
+- Listing the approval rules in project settings.
+- Creating/updating/deleting rules in project settings.
+- Listing the approval rules on create merge request form.
+
+### `Projects::MergeRequests::CreationsController`
+
+This controller is defined in `app/controllers/projects/merge_requests/creations_controller.rb`.
+
+The `create` action of this controller is used when create merge request form is
+submitted. It accepts the `approval_rules_attributes` parameter for creating/updating/deleting
+`ApprovalMergeRequestRule` records. It passes the parameter along when it executes
+`MergeRequests::CreateService`.
+
+### `Projects::MergeRequestsController`
+
+This controller is defined in `app/controllers/projects/merge_requests_controller.rb`.
+
+The `update` action of this controller is used when edit merge request form is
+submitted. It's like `Projects::MergeRequests::CreationsController` but it executes
+`MergeRequests::UpdateService` instead.
+
+### `API::MergeRequestApprovals`
+
+This API is defined in `ee/lib/api/merge_request_approvals.rb`.
+
+The [Approvals API endpoint](../api/merge_request_approvals.md#get-configuration-1)
+is requested when merge request page loads.
+
+The `/projects/:id/merge_requests/:merge_request_iid/approval_settings` is a
+private API endpoint used for the following:
+
+- Listing the approval rules on edit merge request form.
+- Listing the approval rules on the merge request page.
+
+When approving/unapproving MR via UI and API, the [Approve Merge Request](../api/merge_request_approvals.md#approve-merge-request)
+API endpoint and the [Unapprove Merge Request](../api/merge_request_approvals.md#unapprove-merge-request)
+API endpoint are requested. They execute `MergeRequests::ApprovalService` and
+`MergeRequests::RemoveApprovalService` accordingly.
+
+### `API::ProjectApprovalRules` and `API::MergeRequestApprovalRules`
+
+These APIs are defined in `ee/lib/api/project_approval_rules.rb` and
+`ee/lib/api/merge_request_approval_rules.rb`.
+
+Used to list/create/update/delete project and merge request level rules via
+[Merge request approvals API](../api/merge_request_approvals.md).
+
+Executes `ApprovalRules::CreateService`, `ApprovalRules::UpdateService`,
+`ApprovalRules::ProjectRuleDestroyService`, and `ApprovalRules::MergeRequestRuleDestroyService`
+accordingly.
+
+### `ApprovalRules::ParamsFilteringService`
+
+This service is defined in `ee/app/services/approval_rules/params_filtering_service.rb`.
+
+It is called only when `MergeRequests::CreateService` and
+`MergeRequests::UpdateService` are executed.
+
+It is responsible for parsing `approval_rules_attributes` parameter to:
+
+- Remove it when user can't update approval rules.
+- Filter the user IDs whether they are members of the project or not.
+- Filter the group IDs whether they are visible to user.
+- Identify the `any_approver` rule.
+- Append hidden groups to it when specified.
+- Append user defined inapplicable (rules that does not apply to MR's target
+ branch) approval rules.
+
+## Flow
+
+These flowcharts should help explain the flow from the controllers down to the
+models for different functionalities.
+
+Some CRUD API endpoints are intentionally skipped because they are pretty
+straightforward.
+
+### Creating a merge request with approval rules via web UI
+
+```mermaid
+graph LR
+ Projects::MergeRequests::CreationsController --> MergeRequests::CreateService
+ MergeRequests::CreateService --> ApprovalRules::ParamsFilteringService
+ ApprovalRules::ParamsFilteringService --> MergeRequests::CreateService
+ MergeRequests::CreateService --> MergeRequest
+ MergeRequest --> db[(Database)]
+ MergeRequest --> User
+ MergeRequest --> Group
+ MergeRequest --> ApprovalProjectRule
+ User --> db[(Database)]
+ Group --> db[(Database)]
+ ApprovalProjectRule --> db[(Database)]
+```
+
+When updating, same flow is followed but it starts at `Projects::MergeRequestsController`
+and executes `MergeRequests::UpdateService` instead.
+
+### Viewing the merge request approval rules on an MR page
+
+```mermaid
+graph LR
+ API::MergeRequestApprovals --> MergeRequest
+ MergeRequest --> ApprovalState
+ ApprovalState --> id1{approval rules are overridden}
+ id1{approval rules are overridden} --> |No| ApprovalProjectRule & ApprovalMergeRequestRule
+ id1{approval rules are overridden} --> |Yes| ApprovalMergeRequestRule
+ ApprovalState --> ApprovalWrappedRule
+ ApprovalWrappedRule --> Approval
+```
+
+This flow gets initiated by the frontend component. The data returned will
+then be used to display information on the MR widget.
+
+### Approving a merge request
+
+```mermaid
+graph LR
+ API::MergeRequestApprovals --> MergeRequests::ApprovalService
+ MergeRequests::ApprovalService --> Approval
+ Approval --> db[(Database)]
+```
+
+When unapproving, same flow is followed but the `MergeRequests::RemoveApprovalService`
+is executed instead.
+
+## TODO
+
+1. Add information related to other rule types (e.g. `code_owner` and `report_approver`).
+1. Add information about side effects of approving/unapproving merge request.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 5ee11c553af..44802214d7b 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -491,7 +491,10 @@ introduced by [#25381](https://gitlab.com/gitlab-org/gitlab/issues/25381).
### Batch Suggestions
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1 as an [alpha feature](https://about.gitlab.com/handbook/product/#alpha).
+> - It's deployed behind a feature flag, disabled by default.
+> - It's disabled on GitLab.com.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-batch-suggestions).
You can apply multiple suggestions at once to reduce the number of commits added
to your branch to address your reviewers' requests.
@@ -512,6 +515,27 @@ to your branch to address your reviewers' requests.
![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions")
+#### Enable or disable Batch Suggestions
+
+Batch Suggestions is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it for your instance.
+
+To enable it:
+
+```ruby
+# Instance-wide
+Feature.enable(:batched_suggestions)
+```
+
+To disable it:
+
+```ruby
+# Instance-wide
+Feature.disable(:batched_suggestions)
+```
+
## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 38fda74edcf..a59b9cf80e5 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -87,15 +87,17 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
## Repository size limit
-The maximum size your Git repository is allowed to be, including LFS. If you are near
-or over the size limit, you can [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md).
+GitLab.com has the following [account limits](../admin_area/settings/account_and_limit_settings.md) enabled. If a setting is not listed, it is set to the default value.
-| Setting | GitLab.com | Default |
-| ----------- | ----------------- | ------------- |
-| Repository size including LFS | 10G | Unlimited |
+If you are near
+or over the repository size limit, you can [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md).
+
+| Setting | GitLab.com | Default |
+| ----------- | ----------- | ------------- |
+| Repository size including LFS | 10 GB | Unlimited |
NOTE: **Note:**
-`git push` and GitLab project imports are limited to 5GB per request. Git LFS and imports other than a file upload are not affected by this limit.
+`git push` and GitLab project imports are limited to 5 GB per request through Cloudflare. Git LFS and imports other than a file upload are not affected by this limit.
## IP range
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 48975b7864e..0cf375009a0 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -186,7 +186,8 @@ updated every 15 minutes at most, so may not reflect recent activity. The displa
The project size may differ slightly from one instance to another due to compression, housekeeping, and other factors.
-[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins. GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
+[Repository size limit](../../admin_area/settings/account_and_limit_settings.md) may be set by admins.
+GitLab.com's repository size limit [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
## Contributors
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 5f157870151..85a33c608e5 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -328,9 +328,9 @@ module API
user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
- key = user.gpg_keys.new(declared_params(include_missing: false))
+ key = ::GpgKeys::CreateService.new(user, declared_params(include_missing: false)).execute
- if key.save
+ if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
@@ -792,9 +792,9 @@ module API
requires :key, type: String, desc: 'The new GPG key'
end
post 'gpg_keys' do
- key = current_user.gpg_keys.new(declared_params)
+ key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if key.save
+ if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 16689c14815..a05306fbd24 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -37,6 +37,8 @@ module Gitlab
.merge(cycle_analytics_usage_data)
.merge(object_store_usage_data)
.merge(topology_usage_data)
+ .merge(usage_activity_by_stage)
+ .merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, default_time_period))
end
end
@@ -427,6 +429,88 @@ module Gitlab
{ created_at: 28.days.ago..Time.current }
end
+ # Source: https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv
+ def usage_activity_by_stage(key = :usage_activity_by_stage, time_period = {})
+ {
+ key => {
+ configure: usage_activity_by_stage_configure(time_period),
+ create: usage_activity_by_stage_create(time_period),
+ manage: usage_activity_by_stage_manage(time_period),
+ monitor: usage_activity_by_stage_monitor(time_period),
+ package: usage_activity_by_stage_package(time_period),
+ plan: usage_activity_by_stage_plan(time_period),
+ release: usage_activity_by_stage_release(time_period),
+ secure: usage_activity_by_stage_secure(time_period),
+ verify: usage_activity_by_stage_verify(time_period)
+ }
+ }
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def usage_activity_by_stage_configure(time_period)
+ {
+ clusters_applications_cert_managers: cluster_applications_user_distinct_count(::Clusters::Applications::CertManager, time_period),
+ clusters_applications_helm: cluster_applications_user_distinct_count(::Clusters::Applications::Helm, time_period),
+ clusters_applications_ingress: cluster_applications_user_distinct_count(::Clusters::Applications::Ingress, time_period),
+ clusters_applications_knative: cluster_applications_user_distinct_count(::Clusters::Applications::Knative, time_period),
+ clusters_management_project: clusters_user_distinct_count(::Clusters::Cluster.with_management_project, time_period),
+ clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled, time_period),
+ clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled, time_period),
+ clusters_platforms_gke: clusters_user_distinct_count(::Clusters::Cluster.gcp_installed.enabled, time_period),
+ clusters_platforms_eks: clusters_user_distinct_count(::Clusters::Cluster.aws_installed.enabled, time_period),
+ clusters_platforms_user: clusters_user_distinct_count(::Clusters::Cluster.user_provided.enabled, time_period),
+ instance_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.instance_type, time_period),
+ instance_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.instance_type, time_period),
+ group_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.group_type, time_period),
+ group_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.group_type, time_period),
+ project_clusters_disabled: clusters_user_distinct_count(::Clusters::Cluster.disabled.project_type, time_period),
+ project_clusters_enabled: clusters_user_distinct_count(::Clusters::Cluster.enabled.project_type, time_period)
+ }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # Omitted because no user, creator or author associated: `lfs_objects`, `pool_repositories`, `web_hooks`
+ def usage_activity_by_stage_create(time_period)
+ {}
+ end
+
+ # Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`
+ def usage_activity_by_stage_manage(time_period)
+ {}
+ end
+
+ def usage_activity_by_stage_monitor(time_period)
+ {}
+ end
+
+ def usage_activity_by_stage_package(time_period)
+ {}
+ end
+
+ # Omitted because no user, creator or author associated: `boards`, `labels`, `milestones`, `uploads`
+ # Omitted because too expensive: `epics_deepest_relationship_level`
+ # Omitted because of encrypted properties: `projects_jira_cloud_active`, `projects_jira_server_active`
+ def usage_activity_by_stage_plan(time_period)
+ {}
+ end
+
+ # Omitted because no user, creator or author associated: `environments`, `feature_flags`, `in_review_folder`, `pages_domains`
+ def usage_activity_by_stage_release(time_period)
+ {}
+ end
+
+ # Omitted because no user, creator or author associated: `ci_runners`
+ def usage_activity_by_stage_verify(time_period)
+ {}
+ end
+
+ # Currently too complicated and to get reliable counts for these stats:
+ # container_scanning_jobs, dast_jobs, dependency_scanning_jobs, license_management_jobs, sast_jobs, secret_detection_jobs
+ # Once https://gitlab.com/gitlab-org/gitlab/merge_requests/17568 is merged, this might be doable
+ def usage_activity_by_stage_secure(time_period)
+ {}
+ end
+
private
def total_alert_issues
@@ -455,6 +539,16 @@ module Gitlab
clear_memoization(:user_minimum_id)
clear_memoization(:user_maximum_id)
end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def cluster_applications_user_distinct_count(applications, time_period)
+ distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id')
+ end
+
+ def clusters_user_distinct_count(clusters, time_period)
+ distinct_count(clusters.where(time_period), :user_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8fea777c0bf..bacfd5f9e37 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -520,6 +520,9 @@ msgstr ""
msgid "%{primary} (%{secondary})"
msgstr ""
+msgid "%{ref} cannot be added: %{error}"
+msgstr ""
+
msgid "%{releases} release"
msgid_plural "%{releases} releases"
msgstr[0] ""
@@ -2443,6 +2446,9 @@ msgstr ""
msgid "An error occurred while rendering preview broadcast message"
msgstr ""
+msgid "An error occurred while rendering the editor"
+msgstr ""
+
msgid "An error occurred while reordering issues."
msgstr ""
diff --git a/package.json b/package.json
index 7b2f268145f..f6247c79f78 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.140.0",
- "@gitlab/ui": "17.0.1",
+ "@gitlab/ui": "17.1.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
@@ -134,7 +134,7 @@
"tiptap": "^1.8.0",
"tiptap-commands": "^1.4.0",
"tiptap-extensions": "^1.8.0",
- "tributejs": "4.1.3",
+ "tributejs": "5.1.3",
"unfetch": "^4.1.0",
"url-loader": "^3.0.0",
"uuid": "8.1.0",
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 4a7e1ba99e9..eca9e75fed6 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -14,444 +14,629 @@ RSpec.describe 'GFM autocomplete', :js do
let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
- before do
- project.add_maintainer(user)
- project.add_maintainer(user_xss)
+ describe 'when tribute_autocomplete feature flag is off' do
+ before do
+ stub_feature_flags(tribute_autocomplete: false)
- sign_in(user)
- visit project_issue_path(project, issue)
+ project.add_maintainer(user)
+ project.add_maintainer(user_xss)
- wait_for_requests
- end
+ sign_in(user)
+ visit project_issue_path(project, issue)
- it 'updates issue description with GFM reference' do
- find('.js-issuable-edit').click
+ wait_for_requests
+ end
- wait_for_requests
+ it 'updates issue description with GFM reference' do
+ find('.js-issuable-edit').click
- simulate_input('#issue-description', "@#{user.name[0...3]}")
+ wait_for_requests
- wait_for_requests
+ simulate_input('#issue-description', "@#{user.name[0...3]}")
- find('.atwho-view .cur').click
+ wait_for_requests
- click_button 'Save changes'
+ find('.atwho-view .cur').click
- wait_for_requests
+ click_button 'Save changes'
- expect(find('.description')).to have_content(user.to_reference)
- end
+ wait_for_requests
- it 'opens autocomplete menu when field starts with text' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('@')
+ expect(find('.description')).to have_content(user.to_reference)
end
- expect(page).to have_selector('.atwho-container')
- end
-
- it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
- create(:issue, project: project, title: issue_xss_title)
+ it 'opens autocomplete menu when field starts with text' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@')
+ end
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('#')
+ expect(page).to have_selector('.atwho-container')
end
- wait_for_requests
+ it 'opens autocomplete menu for Issues when field starts with text with item escaping HTML characters' do
+ create(:issue, project: project, title: issue_xss_title)
+
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('#')
+ end
- expect(page).to have_selector('.atwho-container')
+ wait_for_requests
- page.within '.atwho-container #at-view-issues' do
- expect(page.all('li').first.text).to include(issue_xss_title)
- end
- end
+ expect(page).to have_selector('.atwho-container')
- it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('@ev')
+ page.within '.atwho-container #at-view-issues' do
+ expect(page.all('li').first.text).to include(issue_xss_title)
+ end
end
- wait_for_requests
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@ev')
+ end
- expect(page).to have_selector('.atwho-container')
+ wait_for_requests
- page.within '.atwho-container #at-view-users' do
- expect(find('li').text).to have_content(user_xss.username)
+ expect(page).to have_selector('.atwho-container')
+
+ page.within '.atwho-container #at-view-users' do
+ expect(find('li').text).to have_content(user_xss.username)
+ end
end
- end
- it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
- create(:milestone, project: project, title: milestone_xss_title)
+ it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
+ create(:milestone, project: project, title: milestone_xss_title)
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('%')
- end
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('%')
+ end
- wait_for_requests
+ wait_for_requests
- expect(page).to have_selector('.atwho-container')
+ expect(page).to have_selector('.atwho-container')
- page.within '.atwho-container #at-view-milestones' do
- expect(find('li').text).to have_content('alert milestone')
+ page.within '.atwho-container #at-view-milestones' do
+ expect(find('li').text).to have_content('alert milestone')
+ end
end
- end
- it 'doesnt open autocomplete menu character is prefixed with text' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('testing')
- find('#note-body').native.send_keys('@')
+ it 'doesnt open autocomplete menu character is prefixed with text' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('testing')
+ find('#note-body').native.send_keys('@')
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
end
- expect(page).not_to have_selector('.atwho-view')
- end
+ it 'doesnt select the first item for non-assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys(':')
+ end
- it 'doesnt select the first item for non-assignee dropdowns' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys(':')
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_requests
+
+ expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
end
- expect(page).to have_selector('.atwho-container')
+ it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
+ note = find('#note-body')
- wait_for_requests
+ # Number.
+ page.within '.timeline-content-form' do
+ note.native.send_keys('7:')
+ end
- expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
- end
+ expect(page).not_to have_selector('.atwho-view')
- it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
- note = find('#note-body')
+ # ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('w:')
+ end
- # Number.
- page.within '.timeline-content-form' do
- note.native.send_keys('7:')
- end
+ expect(page).not_to have_selector('.atwho-view')
- expect(page).not_to have_selector('.atwho-view')
+ # Non-ASCII letter.
+ page.within '.timeline-content-form' do
+ note.set('')
+ note.native.send_keys('Ё:')
+ end
- # ASCII letter.
- page.within '.timeline-content-form' do
- note.set('')
- note.native.send_keys('w:')
+ expect(page).not_to have_selector('.atwho-view')
end
- expect(page).not_to have_selector('.atwho-view')
+ it 'selects the first item for assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@')
+ end
- # Non-ASCII letter.
- page.within '.timeline-content-form' do
- note.set('')
- note.native.send_keys('Ё:')
- end
+ expect(page).to have_selector('.atwho-container')
- expect(page).not_to have_selector('.atwho-view')
- end
+ wait_for_requests
- it 'selects the first item for assignee dropdowns' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('@')
+ expect(find('#at-view-users')).to have_selector('.cur:first-of-type')
end
- expect(page).to have_selector('.atwho-container')
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "@#{user.name[0...8]}")
+ end
- wait_for_requests
+ expect(page).to have_selector('.atwho-container')
- expect(find('#at-view-users')).to have_selector('.cur:first-of-type')
- end
+ wait_for_requests
- it 'includes items for assignee dropdowns with non-ASCII characters in name' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
- simulate_input('#note-body', "@#{user.name[0...8]}")
+ expect(find('#at-view-users')).to have_content(user.name)
end
- expect(page).to have_selector('.atwho-container')
+ it 'selects the first item for non-assignee dropdowns if a query is entered' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys(':1')
+ end
- wait_for_requests
+ expect(page).to have_selector('.atwho-container')
- expect(find('#at-view-users')).to have_content(user.name)
- end
+ wait_for_requests
- it 'selects the first item for non-assignee dropdowns if a query is entered' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys(':1')
+ expect(find('#at-view-58')).to have_selector('.cur:first-of-type')
end
- expect(page).to have_selector('.atwho-container')
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "~#{label.title[0]}")
+ end
- wait_for_requests
+ label_item = find('.atwho-view li', text: label.title)
- expect(find('#at-view-58')).to have_selector('.cur:first-of-type')
- end
+ expect_to_wrap(true, label_item, note, label.title)
+ end
- context 'if a selected value has special characters' do
- it 'wraps the result in double quotes' do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
- simulate_input('#note-body', "~#{label.title[0]}")
+ it "shows dropdown after a new line" do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('test')
+ note.native.send_keys(:enter)
+ note.native.send_keys(:enter)
+ note.native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.atwho-container')
end
- label_item = find('.atwho-view li', text: label.title)
+ it "does not show dropdown when preceded with a special character" do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@")
+ end
- expect_to_wrap(true, label_item, note, label.title)
- end
+ expect(page).to have_selector('.atwho-container')
- it "shows dropdown after a new line" do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys('test')
- note.native.send_keys(:enter)
- note.native.send_keys(:enter)
- note.native.send_keys('@')
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@")
+ end
+
+ expect(page).to have_selector('.atwho-container', visible: false)
end
- expect(page).to have_selector('.atwho-container')
- end
+ it "does not throw an error if no labels exist" do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('~')
+ end
- it "does not show dropdown when preceded with a special character" do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys("@")
+ expect(page).to have_selector('.atwho-container', visible: false)
end
- expect(page).to have_selector('.atwho-container')
+ it 'doesn\'t wrap for assignee values' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@#{user.username[0]}")
+ end
- page.within '.timeline-content-form' do
- note.native.send_keys("@")
+ user_item = find('.atwho-view li', text: user.username)
+
+ expect_to_wrap(false, user_item, note, user.username)
end
- expect(page).to have_selector('.atwho-container', visible: false)
- end
+ it 'doesn\'t wrap for emoji values' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys(":cartwheel_")
+ end
- it "does not throw an error if no labels exist" do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys('~')
+ emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
+
+ expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
- expect(page).to have_selector('.atwho-container', visible: false)
- end
+ it 'doesn\'t open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys("@#{user.username[0..2]}!")
+ end
- it 'doesn\'t wrap for assignee values' do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys("@#{user.username[0]}")
+ expect(page).not_to have_selector('.atwho-view')
end
- user_item = find('.atwho-view li', text: user.username)
+ it 'doesn\'t open autocomplete if there is no space before' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
- expect_to_wrap(false, user_item, note, user.username)
+ it 'triggers autocomplete after selecting a quick action' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('/as')
+ end
+
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
+
+ user_item = find('.atwho-view li', text: user.username)
+ expect(user_item).to have_content(user.username)
+ end
end
- it 'doesn\'t wrap for emoji values' do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys(":cartwheel_")
+ context 'assignees' do
+ let(:issue_assignee) { create(:issue, project: project) }
+ let(:unassigned_user) { create(:user) }
+
+ before do
+ issue_assignee.update(assignees: [user])
+
+ project.add_maintainer(unassigned_user)
end
- emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
+ it 'lists users who are currently not assigned to the issue when using /assign' do
+ visit project_issue_path(project, issue_assignee)
- expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
- end
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('/as')
+ end
- it 'doesn\'t open autocomplete after non-word character' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys("@#{user.username[0..2]}!")
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
+
+ wait_for_requests
+
+ expect(find('#at-view-users .atwho-view-ul')).not_to have_content(user.username)
+ expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
end
- expect(page).not_to have_selector('.atwho-view')
+ it 'shows dropdown on new issue form' do
+ visit new_project_issue_path(project)
+
+ textarea = find('#issue_description')
+ textarea.native.send_keys('/ass')
+ find('.atwho-view li', text: '/assign')
+ textarea.native.send_keys(:tab)
+
+ expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
+ expect(find('#at-view-users .atwho-view-ul')).to have_content(user.username)
+ end
end
- it 'doesn\'t open autocomplete if there is no space before' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
+ context 'labels' do
+ it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
+ create(:label, project: project, title: label_xss_title)
+
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('alert label')
+ end
end
- expect(page).not_to have_selector('.atwho-view')
- end
+ it 'allows colons when autocompleting scoped labels' do
+ create(:label, project: project, title: 'scoped:label')
- it 'triggers autocomplete after selecting a quick action' do
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys('/as')
+ note = find('#note-body')
+ type(note, '~scoped:')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('scoped:label')
+ end
end
- find('.atwho-view li', text: '/assign')
- note.native.send_keys(:tab)
+ it 'allows colons when autocompleting scoped labels with double colons' do
+ create(:label, project: project, title: 'scoped::label')
- user_item = find('.atwho-view li', text: user.username)
- expect(user_item).to have_content(user.username)
- end
- end
+ note = find('#note-body')
+ type(note, '~scoped::')
- context 'assignees' do
- let(:issue_assignee) { create(:issue, project: project) }
- let(:unassigned_user) { create(:user) }
+ wait_for_requests
- before do
- issue_assignee.update(assignees: [user])
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('scoped::label')
+ end
+ end
+
+ it 'allows spaces when autocompleting multi-word labels' do
+ create(:label, project: project, title: 'Accepting merge requests')
+
+ note = find('#note-body')
+ type(note, '~Accepting merge')
+
+ wait_for_requests
- project.add_maintainer(unassigned_user)
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
+ end
+ end
+
+ it 'only autocompletes the latest label' do
+ create(:label, project: project, title: 'Accepting merge requests')
+ create(:label, project: project, title: 'Accepting job applicants')
+
+ note = find('#note-body')
+ type(note, '~Accepting merge requests foo bar ~Accepting job')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
+ end
+ end
+
+ it 'does not autocomplete labels if no tilde is typed' do
+ create(:label, project: project, title: 'Accepting merge requests')
+
+ note = find('#note-body')
+ type(note, 'Accepting merge')
+
+ wait_for_requests
+
+ expect(page).not_to have_css('.atwho-container #at-view-labels')
+ end
end
- it 'lists users who are currently not assigned to the issue when using /assign' do
- visit project_issue_path(project, issue_assignee)
+ shared_examples 'autocomplete suggestions' do
+ it 'suggests objects correctly' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys(object.class.reference_prefix)
+ end
- note = find('#note-body')
- page.within '.timeline-content-form' do
- note.native.send_keys('/as')
+ page.within '.atwho-container' do
+ expect(page).to have_content(object.title)
+
+ find('ul li').click
+ end
+
+ expect(find('.new-note #note-body').value).to include(expected_body)
end
+ end
- find('.atwho-view li', text: '/assign')
- note.native.send_keys(:tab)
+ context 'issues' do
+ let(:object) { issue }
+ let(:expected_body) { object.to_reference }
- wait_for_requests
+ it_behaves_like 'autocomplete suggestions'
+ end
- expect(find('#at-view-users .atwho-view-ul')).not_to have_content(user.username)
- expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
+ context 'merge requests' do
+ let(:object) { create(:merge_request, source_project: project) }
+ let(:expected_body) { object.to_reference }
+
+ it_behaves_like 'autocomplete suggestions'
end
- it 'shows dropdown on new issue form' do
- visit new_project_issue_path(project)
+ context 'project snippets' do
+ let!(:object) { create(:project_snippet, project: project, title: 'code snippet') }
+ let(:expected_body) { object.to_reference }
+
+ it_behaves_like 'autocomplete suggestions'
+ end
- textarea = find('#issue_description')
- textarea.native.send_keys('/ass')
- find('.atwho-view li', text: '/assign')
- textarea.native.send_keys(:tab)
+ context 'label' do
+ let!(:object) { label }
+ let(:expected_body) { object.title }
- expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
- expect(find('#at-view-users .atwho-view-ul')).to have_content(user.username)
+ it_behaves_like 'autocomplete suggestions'
+ end
+
+ context 'milestone' do
+ let!(:object) { create(:milestone, project: project) }
+ let(:expected_body) { object.to_reference }
+
+ it_behaves_like 'autocomplete suggestions'
end
end
- context 'labels' do
- it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
- create(:label, project: project, title: label_xss_title)
+ describe 'when tribute_autocomplete feature flag is on' do
+ before do
+ stub_feature_flags(tribute_autocomplete: true)
- note = find('#note-body')
+ project.add_maintainer(user)
+ project.add_maintainer(user_xss)
- # It should show all the labels on "~".
- type(note, '~')
+ sign_in(user)
+ visit project_issue_path(project, issue)
wait_for_requests
-
- page.within '.atwho-container #at-view-labels' do
- expect(find('.atwho-view-ul').text).to have_content('alert label')
- end
end
- it 'allows colons when autocompleting scoped labels' do
- create(:label, project: project, title: 'scoped:label')
-
- note = find('#note-body')
- type(note, '~scoped:')
+ it 'updates issue description with GFM reference' do
+ find('.js-issuable-edit').click
wait_for_requests
- page.within '.atwho-container #at-view-labels' do
- expect(find('.atwho-view-ul').text).to have_content('scoped:label')
- end
- end
+ simulate_input('#issue-description', "@#{user.name[0...3]}")
- it 'allows colons when autocompleting scoped labels with double colons' do
- create(:label, project: project, title: 'scoped::label')
+ wait_for_requests
- note = find('#note-body')
- type(note, '~scoped::')
+ find('.tribute-container .highlight').click
+
+ click_button 'Save changes'
wait_for_requests
- page.within '.atwho-container #at-view-labels' do
- expect(find('.atwho-view-ul').text).to have_content('scoped::label')
- end
+ expect(find('.description')).to have_content(user.to_reference)
end
- it 'allows spaces when autocompleting multi-word labels' do
- create(:label, project: project, title: 'Accepting merge requests')
+ it 'opens autocomplete menu when field starts with text' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@')
+ end
- note = find('#note-body')
- type(note, '~Accepting merge')
+ expect(page).to have_selector('.tribute-container')
+ end
+
+ it 'opens autocomplete menu for Username when field starts with text with item escaping HTML characters' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@ev')
+ end
wait_for_requests
- page.within '.atwho-container #at-view-labels' do
- expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
+ expect(page).to have_selector('.tribute-container')
+
+ page.within '.tribute-container ul' do
+ expect(find('li').text).to have_content(user_xss.username)
end
end
- it 'only autocompletes the latest label' do
- create(:label, project: project, title: 'Accepting merge requests')
- create(:label, project: project, title: 'Accepting job applicants')
+ it 'doesnt open autocomplete menu character is prefixed with text' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('testing')
+ find('#note-body').native.send_keys('@')
+ end
- note = find('#note-body')
- type(note, '~Accepting merge requests foo bar ~Accepting job')
+ expect(page).not_to have_selector('.tribute-container')
+ end
+
+ it 'selects the first item for assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.tribute-container')
wait_for_requests
- page.within '.atwho-container #at-view-labels' do
- expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
- end
+ expect(find('.tribute-container ul')).to have_selector('.highlight:first-of-type')
end
- it 'does not autocomplete labels if no tilde is typed' do
- create(:label, project: project, title: 'Accepting merge requests')
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "@#{user.name[0...8]}")
+ end
- note = find('#note-body')
- type(note, 'Accepting merge')
+ expect(page).to have_selector('.tribute-container')
wait_for_requests
- expect(page).not_to have_css('.atwho-container #at-view-labels')
+ expect(find('.tribute-container')).to have_content(user.name)
end
- end
- shared_examples 'autocomplete suggestions' do
- it 'suggests objects correctly' do
- page.within '.timeline-content-form' do
- find('#note-body').native.send_keys(object.class.reference_prefix)
+ context 'if a selected value has special characters' do
+ it "shows dropdown after a new line" do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('test')
+ note.native.send_keys(:enter)
+ note.native.send_keys(:enter)
+ note.native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.tribute-container')
end
- page.within '.atwho-container' do
- expect(page).to have_content(object.title)
+ it "does not show dropdown when preceded with a special character" do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@")
+ end
+
+ expect(page).to have_selector('.tribute-container')
- find('ul li').click
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@")
+ end
+
+ expect(page).to have_selector('.tribute-container', visible: false)
end
- expect(find('.new-note #note-body').value).to include(expected_body)
- end
- end
+ it 'doesn\'t wrap for assignee values' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys("@#{user.username[0]}")
+ end
- context 'issues' do
- let(:object) { issue }
- let(:expected_body) { object.to_reference }
+ user_item = find('.tribute-container li', text: user.username)
- it_behaves_like 'autocomplete suggestions'
- end
+ expect_to_wrap(false, user_item, note, user.username)
+ end
- context 'merge requests' do
- let(:object) { create(:merge_request, source_project: project) }
- let(:expected_body) { object.to_reference }
+ it 'doesn\'t open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys("@#{user.username[0..2]}!")
+ end
- it_behaves_like 'autocomplete suggestions'
- end
+ expect(page).not_to have_selector('.tribute-container')
+ end
- context 'project snippets' do
- let!(:object) { create(:project_snippet, project: project, title: 'code snippet') }
- let(:expected_body) { object.to_reference }
+ it 'triggers autocomplete after selecting a quick action' do
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('/as')
+ end
- it_behaves_like 'autocomplete suggestions'
- end
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
+ note.native.send_keys(:right)
- context 'label' do
- let!(:object) { label }
- let(:expected_body) { object.title }
+ wait_for_requests
- it_behaves_like 'autocomplete suggestions'
- end
+ user_item = find('.tribute-container li', text: user.username)
+ expect(user_item).to have_content(user.username)
+ end
+ end
- context 'milestone' do
- let!(:object) { create(:milestone, project: project) }
- let(:expected_body) { object.to_reference }
+ context 'assignees' do
+ let(:issue_assignee) { create(:issue, project: project) }
+ let(:unassigned_user) { create(:user) }
+
+ before do
+ issue_assignee.update(assignees: [user])
+
+ project.add_maintainer(unassigned_user)
+ end
- it_behaves_like 'autocomplete suggestions'
+ it 'lists users who are currently not assigned to the issue when using /assign' do
+ visit project_issue_path(project, issue_assignee)
+
+ note = find('#note-body')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('/as')
+ end
+
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
+ note.native.send_keys(:right)
+
+ wait_for_requests
+
+ expect(find('.tribute-container ul')).not_to have_content(user.username)
+ expect(find('.tribute-container ul')).to have_content(unassigned_user.username)
+ end
+ end
end
private
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 4db1633abe6..1d5e6e55601 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
end
it 'allows committing to the source branch' do
- find('.ace_text-input', visible: false).send_keys('Updated the readme')
+ execute_script("monaco.editor.getModels()[0].setValue('Updated the readme')")
click_button 'Commit changes'
wait_for_requests
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 5c29ac870c0..d6f23b21d65 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -36,12 +36,35 @@ RSpec.describe 'Member autocomplete', :js do
let(:noteable) { create(:issue, author: author, project: project) }
before do
+ stub_feature_flags(tribute_autocomplete: false)
visit project_issue_path(project, noteable)
end
include_examples "open suggestions when typing @", 'issue'
end
+ describe 'when tribute_autocomplete feature flag is on' do
+ context 'adding a new note on a Issue' do
+ let(:noteable) { create(:issue, author: author, project: project) }
+
+ before do
+ stub_feature_flags(tribute_autocomplete: true)
+ visit project_issue_path(project, noteable)
+
+ page.within('.new-note') do
+ find('#note-body').send_keys('@')
+ end
+ end
+
+ it 'suggests noteable author and note author' do
+ page.within('.tribute-container', visible: true) do
+ expect(page).to have_content(author.username)
+ expect(page).to have_content(note.author.username)
+ end
+ end
+ end
+ end
+
context 'adding a new note on a Merge Request' do
let(:project) { create(:project, :public, :repository) }
let(:noteable) do
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 56bf31f24ba..5851121f635 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -36,8 +36,7 @@ RSpec.describe 'Editing file blob', :js do
def fill_editor(content: 'class NextFeature\\nend\\n')
wait_for_requests
- find('#editor')
- execute_script("ace.edit('editor').setValue('#{content}')")
+ execute_script("monaco.editor.getModels()[0].setValue('#{content}')")
end
context 'from MR diff' do
@@ -67,6 +66,15 @@ RSpec.describe 'Editing file blob', :js do
expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml')
end
+ it 'updating file path updates syntax highlighting' do
+ visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
+ expect(find('#editor')['data-mode-id']).to eq('markdown')
+
+ find('#file_path').send_keys('foo.txt') do
+ expect(find('#editor')['data-mode-id']).to eq('plaintext')
+ end
+ end
+
context 'from blob file path' do
before do
stub_feature_flags(code_navigation: false)
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
index f54bceec2b3..8d107e52db2 100644
--- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -16,8 +16,7 @@ RSpec.describe 'User creates blob in new project', :js do
it 'allows the user to add a new file' do
click_link 'New file'
- find('#editor')
- execute_script('ace.edit("editor").setValue("Hello world")')
+ execute_script("monaco.editor.getModels()[0].setValue('Hello world')")
fill_in(:file_name, with: 'dummy-file')
diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
index 5270774b541..8b43687c71c 100644
--- a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
+++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
@@ -32,6 +32,8 @@ RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled
end
it 'displays suggest_gitlab_ci_yml popover' do
+ page.find(:css, '.gitlab-ci-yml-selector').click
+
popover_selector = '.suggest-gitlab-ci-yml'
expect(page).to have_css(popover_selector, visible: true)
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index ede22204dbd..fda024e893d 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -8,8 +8,9 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
user = project.owner
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
+
page.within('.file-editor.code') do
- find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
+ find('.inputarea', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
run away chase the pig around the house eat owner\'s food, and knock
dish off table head butt cant eat out of my own dish. Cat is love, cat
is life rub face on everything poop on grasses so meow. Playing with
@@ -26,17 +27,20 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
it 'user clicks the "Soft wrap" button and then "No wrap" button' do
wrapped_content_width = get_content_width
- toggle_button.click
- expect(toggle_button).to have_content 'No wrap'
- unwrapped_content_width = get_content_width
- expect(unwrapped_content_width).to be < wrapped_content_width
-
- toggle_button.click
- expect(toggle_button).to have_content 'Soft wrap'
- expect(get_content_width).to be > unwrapped_content_width
+
+ toggle_button.click do
+ expect(toggle_button).to have_content 'Soft wrap'
+ unwrapped_content_width = get_content_width
+ expect(unwrapped_content_width).to be > wrapped_content_width
+ end
+
+ toggle_button.click do
+ expect(toggle_button).to have_content 'No wrap'
+ expect(get_content_width).to be < unwrapped_content_width
+ end
end
def get_content_width
- find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
+ find('.view-lines', visible: false)[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end
end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index dae1164f7f2..5a39f2bcd98 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -25,6 +25,6 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('/.bundle')
- expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
+ expect(page).to have_content('config/initializers/secret_token.rb')
end
end
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index 7d412730115..9c86f2dd190 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md'
- find('.ace_text-input', visible: false).send_keys.native.send_keys options[:file_content] || 'Some content'
+ find('.monaco-editor textarea').send_keys.native.send_keys options[:file_content] || 'Some content'
click_button 'Commit changes'
end
@@ -89,7 +89,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file' do
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -105,7 +105,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file with new lines at the end of file' do
find('#editor')
- execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
+ execute_script('monaco.editor.getModels()[0].setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -117,7 +117,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
find('.js-edit-blob').click
find('#editor')
- expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
+ expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq("Sample\n\n\n")
end
it 'creates and commit a new file with a directory name' do
@@ -126,7 +126,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -141,7 +141,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
@@ -176,7 +176,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor')
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 1bb931e35ec..c8461468c52 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -46,9 +46,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
- expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
+ expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end
it 'does not show the edit link if a file is binary' do
@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -85,7 +85,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
@@ -103,7 +103,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
click_link('Preview changes')
expect(page).to have_css('.line_holder.new')
@@ -148,9 +148,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
- expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
+ expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
@@ -178,7 +178,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first)
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -207,7 +207,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect(page).not_to have_button('Cancel')
find('#editor')
- execute_script("ace.edit('editor').setValue('*.rbca')")
+ execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
diff --git a/spec/frontend/issue_show/components/pinned_links_spec.js b/spec/frontend/issue_show/components/pinned_links_spec.js
index 007ad4c9a1b..bb67feee601 100644
--- a/spec/frontend/issue_show/components/pinned_links_spec.js
+++ b/spec/frontend/issue_show/components/pinned_links_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
+import { STATUS_PAGE_PUBLISHED, JOIN_ZOOM_MEETING } from '~/issue_show/constants';
const plainZoomUrl = 'https://zoom.us/j/123456789';
const plainStatusUrl = 'https://status.com';
@@ -8,7 +9,7 @@ const plainStatusUrl = 'https://status.com';
describe('PinnedLinks', () => {
let wrapper;
- const findLinks = () => wrapper.findAll(GlLink);
+ const findButtons = () => wrapper.findAll(GlButton);
const createComponent = props => {
wrapper = shallowMount(PinnedLinks, {
@@ -26,10 +27,10 @@ describe('PinnedLinks', () => {
});
expect(
- findLinks()
+ findButtons()
.at(0)
.text(),
- ).toBe('Join Zoom meeting');
+ ).toBe(JOIN_ZOOM_MEETING);
});
it('displays Status link', () => {
@@ -38,10 +39,10 @@ describe('PinnedLinks', () => {
});
expect(
- findLinks()
+ findButtons()
.at(0)
.text(),
- ).toBe('Published on status page');
+ ).toBe(STATUS_PAGE_PUBLISHED);
});
it('does not render if there are no links', () => {
@@ -50,6 +51,6 @@ describe('PinnedLinks', () => {
publishedIncidentUrl: '',
});
- expect(wrapper.find(GlLink).exists()).toBe(false);
+ expect(findButtons()).toHaveLength(0);
});
});
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index bbbffc1de4c..85f68c03252 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -10,7 +10,73 @@ describe Gitlab::UsageData, :aggregate_failures do
stub_object_store_settings
end
- describe '#uncached_data' do
+ describe '.uncached_data' do
+ describe '.usage_activity_by_stage' do
+ it 'includes usage_activity_by_stage data' do
+ expect(described_class.uncached_data).to include(:usage_activity_by_stage)
+ expect(described_class.uncached_data).to include(:usage_activity_by_stage_monthly)
+ end
+
+ context 'for configure' do
+ it 'includes accurate usage_activity_by_stage data' do
+ for_defined_days_back do
+ user = create(:user)
+ cluster = create(:cluster, user: user)
+ create(:clusters_applications_cert_manager, :installed, cluster: cluster)
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ create(:clusters_applications_ingress, :installed, cluster: cluster)
+ create(:clusters_applications_knative, :installed, cluster: cluster)
+ create(:cluster, :disabled, user: user)
+ create(:cluster_provider_gcp, :created)
+ create(:cluster_provider_aws, :created)
+ create(:cluster_platform_kubernetes)
+ create(:cluster, :group, :disabled, user: user)
+ create(:cluster, :group, user: user)
+ create(:cluster, :instance, :disabled, :production_environment)
+ create(:cluster, :instance, :production_environment)
+ create(:cluster, :management_project)
+ end
+
+ expect(described_class.uncached_data[:usage_activity_by_stage][:configure]).to include(
+ clusters_applications_cert_managers: 2,
+ clusters_applications_helm: 2,
+ clusters_applications_ingress: 2,
+ clusters_applications_knative: 2,
+ clusters_management_project: 2,
+ clusters_disabled: 4,
+ clusters_enabled: 12,
+ clusters_platforms_gke: 2,
+ clusters_platforms_eks: 2,
+ clusters_platforms_user: 2,
+ instance_clusters_disabled: 2,
+ instance_clusters_enabled: 2,
+ group_clusters_disabled: 2,
+ group_clusters_enabled: 2,
+ project_clusters_disabled: 2,
+ project_clusters_enabled: 10
+ )
+ expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:configure]).to include(
+ clusters_applications_cert_managers: 1,
+ clusters_applications_helm: 1,
+ clusters_applications_ingress: 1,
+ clusters_applications_knative: 1,
+ clusters_management_project: 1,
+ clusters_disabled: 2,
+ clusters_enabled: 6,
+ clusters_platforms_gke: 1,
+ clusters_platforms_eks: 1,
+ clusters_platforms_user: 1,
+ instance_clusters_disabled: 1,
+ instance_clusters_enabled: 1,
+ group_clusters_disabled: 1,
+ group_clusters_enabled: 1,
+ project_clusters_disabled: 1,
+ project_clusters_enabled: 5
+ )
+ end
+ end
+ end
+
it 'ensures recorded_at is set before any other usage data calculation' do
%i(alt_usage_data redis_usage_data distinct_count count).each do |method|
expect(described_class).not_to receive(method)
@@ -598,4 +664,12 @@ describe Gitlab::UsageData, :aggregate_failures do
)
end
end
+
+ def for_defined_days_back(days: [29, 2])
+ days.each do |n|
+ Timecop.travel(n.days.ago) do
+ yield
+ end
+ end
+ end
end
diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb
index 3f3b8046232..ad055e20af1 100644
--- a/spec/models/plan_spec.rb
+++ b/spec/models/plan_spec.rb
@@ -14,4 +14,16 @@ describe Plan do
end
end
end
+
+ context 'when updating plan limits' do
+ let(:plan) { described_class.default }
+
+ it { expect(plan).to be_persisted }
+
+ it { expect(plan.actual_limits).not_to be_persisted }
+
+ it 'successfully updates the limits' do
+ expect(plan.actual_limits.update!(ci_instance_level_variables: 100)).to be_truthy
+ end
+ end
end
diff --git a/yarn.lock b/yarn.lock
index 50b0249d7fb..902ee0246ce 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -840,10 +840,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.140.0.tgz#593f1f65b0df57c3399fcfb9f472f59aa64da074"
integrity sha512-6gANJGi2QkpvOgFTMcY3SIwEqhO69i6R3jU4BSskkVziwDdAWxGonln22a4Iu//Iv0NrsFDpAA0jIVfnJzw0iA==
-"@gitlab/ui@17.0.1":
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.0.1.tgz#daf036dfdc095f94123c80c3fb1ab5fe4dcbf95b"
- integrity sha512-JSUGruV6oploADF0Sc0BBY43Des3utU9iWCnR8BAmttKFXFFNUKwTf908yZPGJtfnVyjJkVioOCOYkvUZ0jngg==
+"@gitlab/ui@17.1.0":
+ version "17.1.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.1.0.tgz#522912caee7689a0fde1e58cd6d4e4163e39ca7f"
+ integrity sha512-KruPE0I4qU4LP+pPIzhCY0xbNDcB7gUHaXMO1we2ssK9lc+NJxeLt9gr/ctOX1/Rzgau93+i9QvekeNg0wvqGA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
@@ -11507,10 +11507,10 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
-tributejs@4.1.3:
- version "4.1.3"
- resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-4.1.3.tgz#2e1be7d9a1e403ed4c394f91d859812267e4691c"
- integrity sha512-+VUqyi8p7tCdaqCINCWHf95E2hJFMIML180BhplTpXNooz3E2r96AONXI9qO2Ru6Ugp7MsMPJjB+rnBq+hAmzA==
+tributejs@5.1.3:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
+ integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
trim-newlines@^1.0.0:
version "1.0.0"