diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-23 00:08:53 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-23 00:08:53 +0300 |
commit | 386dcdbe9d3cef9d5fa79c4582a722db27fe2c57 (patch) | |
tree | a0719a1794b21fedf33ab51a4052b8b9c0f7b573 /app | |
parent | a9a2f9257eae40935e03ca4185d5263bcb7ba45f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
9 files changed, 359 insertions, 8 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 50266e2c434..b5e446d13e4 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -15,6 +15,7 @@ import { MR_COMMITS_PREVIOUS_COMMIT, } from '~/behaviors/shortcuts/keybindings'; import { createAlert } from '~/alert'; +import { InternalEvents } from '~/tracking'; import { isSingleViewStyle } from '~/helpers/diffs_helper'; import { helpPagePath } from '~/helpers/help_page_helper'; import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils'; @@ -86,7 +87,7 @@ export default { GlSprintf, GlAlert, }, - mixins: [glFeatureFlagsMixin()], + mixins: [glFeatureFlagsMixin(), InternalEvents.mixin()], alerts: { ALERT_OVERFLOW_HIDDEN, ALERT_MERGE_CONFLICT, @@ -443,6 +444,8 @@ export default { notesEventHub.$once('fetchDiffData', this.fetchData); notesEventHub.$on('refetchDiffData', this.refetchDiffData); notesEventHub.$on('fetchedNotesData', this.rereadNoteHash); + notesEventHub.$on('noteFormAddToReview', this.handleReviewTracking); + notesEventHub.$on('noteFormStartReview', this.handleReviewTracking); diffsEventHub.$on('diffFilesModified', this.setDiscussions); diffsEventHub.$on('doneLoadingBatches', this.autoScroll); diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); @@ -453,6 +456,8 @@ export default { diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); diffsEventHub.$off('doneLoadingBatches', this.autoScroll); diffsEventHub.$off('diffFilesModified', this.setDiscussions); + notesEventHub.$off('noteFormStartReview', this.handleReviewTracking); + notesEventHub.$off('noteFormAddToReview', this.handleReviewTracking); notesEventHub.$off('fetchedNotesData', this.rereadNoteHash); notesEventHub.$off('refetchDiffData', this.refetchDiffData); notesEventHub.$off('fetchDiffData', this.fetchData); @@ -679,6 +684,16 @@ export default { reloadPage() { window.location.reload(); }, + handleReviewTracking(event) { + const types = { + noteFormStartReview: 'merge_request_click_start_review_on_changes_tab', + noteFormAddToReview: 'merge_request_click_add_to_review_on_changes_tab', + }; + + if (this.shouldShow && types[event.name]) { + this.trackEvent(types[event.name]); + } + }, }, howToMergeDocsPath: helpPagePath('user/project/merge_requests/reviews/index.md', { anchor: 'checkout-merge-requests-locally-through-the-head-ref', diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js index 6484fcff769..9bb2884e065 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js @@ -401,6 +401,8 @@ export const nWeeksBefore = (date, numberOfWeeks, options) => /** * Returns the date `n` years after the date provided. + * When Feb 29 is the specified date, the default behaviour is to return March 1. + * But to align with the equivalent rails code, moment JS and datefns we should return Feb 28 instead. * * @param {Date} date the initial date * @param {Number} numberOfYears number of years after @@ -408,7 +410,16 @@ export const nWeeksBefore = (date, numberOfWeeks, options) => */ export const nYearsAfter = (date, numberOfYears) => { const clone = newDate(date); - clone.setFullYear(clone.getFullYear() + numberOfYears); + clone.setUTCMonth(clone.getUTCMonth()); + + // If the date we are calculating from is Feb 29, return the equivalent result for Feb 28 + if (clone.getUTCMonth() === 1 && clone.getUTCDate() === 29) { + clone.setUTCDate(28); + } else { + clone.setUTCDate(clone.getUTCDate()); + } + + clone.setUTCFullYear(clone.getUTCFullYear() + numberOfYears); return clone; }; diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 87b55b19c08..17eded3bec0 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -12,6 +12,7 @@ import { slugifyWithUnderscore, } from '~/lib/utils/text_utility'; import { sprintf } from '~/locale'; +import { InternalEvents } from '~/tracking'; import { badgeState } from '~/merge_requests/components/merge_request_header.vue'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -48,7 +49,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [issuableStateMixin], + mixins: [issuableStateMixin, InternalEvents.mixin()], props: { noteableType: { type: String, @@ -253,6 +254,10 @@ export default { this.isSubmitting = true; + if (isDraft) { + eventHub.$emit('noteFormAddToReview', { name: 'noteFormAddToReview' }); + } + trackSavedUsingEditor( this.$refs.markdownEditor.isContentEditorActive, `${this.noteableType}_${this.noteType}`, diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 77ce5ea5910..135d595aae5 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -320,12 +320,14 @@ export default { ); }, handleAddToReview() { + const clickType = this.hasDrafts ? 'noteFormAddToReview' : 'noteFormStartReview'; // check if draft should resolve thread const shouldResolve = (this.discussionResolved && !this.isUnresolving) || (!this.discussionResolved && this.isResolving); this.isSubmitting = true; + eventHub.$emit(clickType, { name: clickType }); this.$emit( 'handleFormUpdateAddToReview', this.updatedNoteBody, diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 86f93ee425e..eb6764a7937 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -2,6 +2,7 @@ // eslint-disable-next-line no-restricted-imports import { mapGetters, mapActions } from 'vuex'; import { v4 as uuidv4 } from 'uuid'; +import { InternalEvents } from '~/tracking'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; @@ -39,7 +40,7 @@ export default { TimelineEntryItem, AiSummary: () => import('ee_component/notes/components/ai_summary.vue'), }, - mixins: [glFeatureFlagsMixin()], + mixins: [glFeatureFlagsMixin(), InternalEvents.mixin()], provide() { return { summarizeClientSubscriptionId: uuidv4(), @@ -165,6 +166,9 @@ export default { }); } + eventHub.$on('noteFormAddToReview', this.handleReviewTracking); + eventHub.$on('noteFormStartReview', this.handleReviewTracking); + window.addEventListener('hashchange', this.handleHashChanged); eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality); @@ -177,6 +181,8 @@ export default { beforeDestroy() { window.removeEventListener('hashchange', this.handleHashChanged); eventHub.$off('notesApp.updateIssuableConfidentiality', this.setConfidentiality); + eventHub.$off('noteFormStartReview', this.handleReviewTracking); + eventHub.$off('noteFormAddToReview', this.handleReviewTracking); }, methods: { ...mapActions([ @@ -222,6 +228,16 @@ export default { setAiLoading(loading) { this.aiLoading = loading; }, + handleReviewTracking(event) { + const types = { + noteFormStartReview: 'merge_request_click_start_review_on_overview_tab', + noteFormAddToReview: 'merge_request_click_add_to_review_on_overview_tab', + }; + + if (this.shouldShow && window.mrTabs && types[event.name]) { + this.trackEvent(types[event.name]); + } + }, }, systemNote: constants.SYSTEM_NOTE, }; diff --git a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue index a812b90e378..1594e125da3 100644 --- a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue +++ b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue @@ -1,20 +1,23 @@ <script> -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlKeysetPagination } from '@gitlab/ui'; import StorageUsageStatistics from 'ee_else_ce/usage_quotas/storage/components/storage_usage_statistics.vue'; import SearchAndSortBar from '~/usage_quotas/components/search_and_sort_bar/search_and_sort_bar.vue'; import DependencyProxyUsage from './dependency_proxy_usage.vue'; import ContainerRegistryUsage from './container_registry_usage.vue'; +import ProjectList from './project_list.vue'; export default { name: 'NamespaceStorageApp', components: { GlAlert, + GlKeysetPagination, StorageUsageStatistics, DependencyProxyUsage, ContainerRegistryUsage, SearchAndSortBar, + ProjectList, }, - inject: ['userNamespace', 'namespaceId'], + inject: ['userNamespace', 'namespaceId', 'helpLinks', 'defaultPerPage'], props: { namespaceLoadingError: { type: Boolean, @@ -31,11 +34,26 @@ export default { required: false, default: false, }, + isNamespaceProjectsLoading: { + type: Boolean, + required: false, + default: false, + }, namespace: { type: Object, required: false, default: () => ({}), }, + projects: { + type: Object, + required: false, + default: () => ({}), + }, + initialSortBy: { + type: String, + required: false, + default: 'storage', + }, }, computed: { usedStorage() { @@ -55,6 +73,27 @@ export default { containerRegistrySizeIsEstimated() { return this.namespace.rootStorageStatistics?.containerRegistrySizeIsEstimated ?? false; }, + projectList() { + return this.projects?.nodes ?? []; + }, + pageInfo() { + return this.projects?.pageInfo; + }, + showPagination() { + return Boolean(this.pageInfo?.hasPreviousPage || this.pageInfo?.hasNextPage); + }, + }, + methods: { + onPrev(before) { + if (this.pageInfo?.hasPreviousPage) { + this.$emit('fetch-more-projects', { before, last: this.defaultPerPage, first: undefined }); + } + }, + onNext(after) { + if (this.pageInfo?.hasNextPage) { + this.$emit('fetch-more-projects', { after, first: this.defaultPerPage }); + } + }, }, }; </script> @@ -103,7 +142,26 @@ export default { " /> </div> - <slot name="ee-storage-app"></slot> + <project-list + :projects="projectList" + :is-loading="isNamespaceProjectsLoading" + :help-links="helpLinks" + :sort-by="initialSortBy" + :sort-desc="true" + @sortChanged=" + ($event) => { + $emit('sort-changed', $event); + } + " + /> + <div class="gl-display-flex gl-justify-content-center gl-mt-5"> + <gl-keyset-pagination + v-if="showPagination" + v-bind="pageInfo" + @prev="onPrev" + @next="onNext" + /> + </div> </section> </div> </template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_list.vue b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue new file mode 100644 index 00000000000..c6f9b1fff03 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue @@ -0,0 +1,208 @@ +<script> +import { GlTable, GlLink, GlSprintf, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; +import { containerRegistryPopover } from '~/usage_quotas/storage/constants'; +import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue'; +import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue'; +import StorageTypeHelpLink from './storage_type_help_link.vue'; +import StorageTypeWarning from './storage_type_warning.vue'; + +export default { + name: 'ProjectList', + components: { + GlTable, + GlLink, + GlSprintf, + GlIcon, + ProjectAvatar, + NumberToHumanSize, + HelpPageLink, + StorageTypeHelpLink, + StorageTypeWarning, + }, + inject: ['isUsingProjectEnforcementWithLimits'], + props: { + projects: { + type: Array, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + helpLinks: { + type: Object, + required: true, + }, + sortBy: { + type: String, + required: false, + default: undefined, + }, + sortDesc: { + type: Boolean, + required: false, + default: undefined, + }, + }, + created() { + this.fields = [ + { key: 'name', label: __('Project') }, + { key: 'storage', label: __('Total'), sortable: !this.isUsingProjectEnforcementWithLimits }, + { key: 'repository', label: __('Repository') }, + { key: 'snippets', label: __('Snippets') }, + { key: 'buildArtifacts', label: __('Jobs') }, + { key: 'lfsObjects', label: __('LFS') }, + { key: 'packages', label: __('Packages') }, + { key: 'wiki', label: __('Wiki') }, + { + key: 'containerRegistry', + label: __('Containers'), + thClass: 'gl-border-l!', + tdClass: 'gl-border-l!', + }, + ].map((f) => ({ + ...f, + // eslint-disable-next-line @gitlab/require-i18n-strings + thClass: `${f.thClass ?? ''} gl-px-3!`, + // eslint-disable-next-line @gitlab/require-i18n-strings + tdClass: `${f.tdClass ?? ''} gl-px-3!`, + })); + }, + methods: { + /** + * Builds a gl-table td cell slot name for particular field + * @param {string} key + * @returns {string} */ + getHeaderSlotName(key) { + return `head(${key})`; + }, + getUsageQuotasUrl(projectUrl) { + return `${projectUrl}/-/usage_quotas`; + }, + /** + * Creates a relative path from a full project path. + * E.g. input `namespace / subgroup / project` + * results in `subgroup / project` + */ + getProjectRelativePath(fullPath) { + return fullPath.replace(/.*?\s?\/\s?/, ''); + }, + isCostFactored(project) { + return project.statistics.storageSize !== project.statistics.costFactoredStorageSize; + }, + }, + containerRegistryPopover, +}; +</script> + +<template> + <gl-table + :fields="fields" + :items="projects" + :busy="isLoading" + show-empty + :empty-text="s__('UsageQuota|No projects to display.')" + small + stacked="lg" + :sort-by="sortBy" + :sort-desc="sortDesc" + no-local-sorting + no-sort-reset + @sort-changed="$emit('sortChanged', $event)" + > + <template v-for="field in fields" #[getHeaderSlotName(field.key)]> + <div :key="field.key" :data-testid="'th-' + field.key"> + {{ field.label }} + + <storage-type-help-link + v-if="field.key in helpLinks" + :storage-type="field.key" + :help-links="helpLinks" + /><storage-type-warning v-if="field.key == 'containerRegistry'"> + {{ $options.containerRegistryPopover.content }} + <gl-link :href="$options.containerRegistryPopover.docsLink" target="_blank"> + {{ __('Learn more.') }} + </gl-link> + </storage-type-warning> + </div> + </template> + + <template #cell(name)="{ item: project }"> + <project-avatar + :project-id="project.id" + :project-name="project.name" + :project-avatar-url="project.avatarUrl" + :size="16" + :alt="project.name" + class="gl-display-inline-block gl-mr-2 gl-text-center!" + /> + + <gl-link + :href="getUsageQuotasUrl(project.webUrl)" + class="gl-text-gray-900! js-project-link gl-word-break-word" + data-testid="project-link" + > + {{ getProjectRelativePath(project.nameWithNamespace) }} + </gl-link> + </template> + + <template #cell(storage)="{ item: project }"> + <template v-if="isCostFactored(project)"> + <number-to-human-size :value="project.statistics.costFactoredStorageSize" /> + + <div class="gl-text-gray-600 gl-mt-2 gl-font-sm"> + <gl-sprintf :message="s__('UsageQuotas|(of %{totalStorageSize})')"> + <template #totalStorageSize> + <number-to-human-size :value="project.statistics.storageSize" /> + </template> + </gl-sprintf> + <help-page-link href="user/usage_quotas#view-project-fork-storage-usage" target="_blank"> + <gl-icon name="question-o" :size="12" /> + </help-page-link> + </div> + </template> + <template v-else> + <number-to-human-size :value="project.statistics.storageSize" /> + </template> + </template> + + <template #cell(repository)="{ item: project }"> + <number-to-human-size + :value="project.statistics.repositorySize" + data-testid="project-repository-size" + /> + </template> + + <template #cell(lfsObjects)="{ item: project }"> + <number-to-human-size :value="project.statistics.lfsObjectsSize" /> + </template> + + <template #cell(buildArtifacts)="{ item: project }"> + <number-to-human-size :value="project.statistics.buildArtifactsSize" /> + </template> + + <template #cell(packages)="{ item: project }"> + <number-to-human-size :value="project.statistics.packagesSize" /> + </template> + + <template #cell(wiki)="{ item: project }"> + <number-to-human-size :value="project.statistics.wikiSize" data-testid="project-wiki-size" /> + </template> + + <template #cell(snippets)="{ item: project }"> + <number-to-human-size + :value="project.statistics.snippetsSize" + data-testid="project-snippets-size" + /> + </template> + + <template #cell(containerRegistry)="{ item: project }"> + <number-to-human-size + :value="project.statistics.containerRegistrySize" + data-testid="project-containers-registry-size" + /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue new file mode 100644 index 00000000000..c25b1848124 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue @@ -0,0 +1,36 @@ +<script> +import { GlLink, GlIcon } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import { HELP_LINK_ARIA_LABEL } from '~/usage_quotas/storage/constants'; + +export default { + name: 'StorageTypeHelpLink', + components: { + GlLink, + GlIcon, + }, + props: { + storageType: { + type: String, + required: true, + }, + helpLinks: { + type: Object, + required: true, + }, + }, + computed: { + ariaLabel() { + return sprintf(HELP_LINK_ARIA_LABEL, { + linkTitle: this.storageType, + }); + }, + }, +}; +</script> + +<template> + <gl-link :href="helpLinks[storageType]" target="_blank" :aria-label="ariaLabel"> + <gl-icon name="question-o" :size="12" /> + </gl-link> +</template> diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb index 72d0c022609..7243bc411f7 100644 --- a/app/services/service_ping/submit_service.rb +++ b/app/services/service_ping/submit_service.rb @@ -16,7 +16,7 @@ module ServicePing end def execute - return unless ServicePing::ServicePingSettings.product_intelligence_enabled? + return unless ServicePing::ServicePingSettings.enabled_and_consented? start_time = Time.current |