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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-23 00:08:53 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-23 00:08:53 +0300
commit386dcdbe9d3cef9d5fa79c4582a722db27fe2c57 (patch)
treea0719a1794b21fedf33ab51a4052b8b9c0f7b573 /app
parenta9a2f9257eae40935e03ca4185d5263bcb7ba45f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diffs/components/app.vue17
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js13
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue7
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue18
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue64
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_list.vue208
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue36
-rw-r--r--app/services/service_ping/submit_service.rb2
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