diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-13 12:10:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-13 12:10:22 +0300 |
commit | 10e15ac3c2798956ff6a43d7b36bdf86c68aa817 (patch) | |
tree | 491a448439d1c2f43258d0418c53e8cc00b51039 /app | |
parent | 1cd5d53f92b07b0be71b6c2d9fdfa7cf07221890 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
21 files changed, 545 insertions, 169 deletions
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 247910301e7..960c8e472b8 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,15 +1,10 @@ <script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, - GlIntersectionObserver, - GlLoadingIcon, -} from '@gitlab/ui'; -import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlCollapsibleListbox } from '@gitlab/ui'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { debounce } from 'lodash'; import { s__ } from '~/locale'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { ListType } from '../constants'; export default { @@ -27,12 +22,7 @@ export default { order_by: 'similarity', }, components: { - GlIntersectionObserver, - GlLoadingIcon, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, + GlCollapsibleListbox, }, inject: ['groupId'], props: { @@ -44,6 +34,7 @@ export default { data() { return { initialLoading: true, + selectedProjectId: '', selectedProject: {}, searchTerm: '', }; @@ -51,6 +42,12 @@ export default { computed: { ...mapState(['groupProjectsFlags']), ...mapGetters(['activeGroupProjects']), + projects() { + return this.activeGroupProjects.map((project) => ({ + value: project.id, + text: project.nameWithNamespace, + })); + }, selectedProjectName() { return this.selectedProject.name || this.$options.i18n.dropdownText; }, @@ -73,26 +70,27 @@ export default { }, }, watch: { - searchTerm() { + searchTerm: debounce(function debouncedSearch() { this.fetchGroupProjects({ search: this.searchTerm }); - }, + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), }, mounted() { this.fetchGroupProjects({}); - this.initialLoading = false; }, methods: { ...mapActions(['fetchGroupProjects', 'setSelectedProject']), selectProject(projectId) { + this.selectedProjectId = projectId; this.selectedProject = this.activeGroupProjects.find((project) => project.id === projectId); this.setSelectedProject(this.selectedProject); }, loadMoreProjects() { + if (!this.hasNextPage) return; this.fetchGroupProjects({ search: this.searchTerm, fetchNext: true }); }, - setFocus() { - this.$refs.search.focusInput(); + onSearch(query) { + this.searchTerm = query; }, }, }; @@ -103,45 +101,23 @@ export default { <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{ $options.i18n.headerTitle }}</label> - <gl-dropdown + <gl-collapsible-listbox + v-model="selectedProjectId" + block + searchable + infinite-scroll data-testid="project-select-dropdown" - :text="selectedProjectName" + :items="projects" + :toggle-text="selectedProjectName" :header-text="$options.i18n.headerTitle" - block - menu-class="gl-w-full!" :loading="initialLoading" - @shown="setFocus" - > - <gl-search-box-by-type - ref="search" - v-model.trim="searchTerm" - debounce="250" - :placeholder="$options.i18n.searchPlaceholder" - /> - <gl-dropdown-item - v-for="project in activeGroupProjects" - v-show="!groupProjectsFlags.isLoading" - :key="project.id" - :name="project.name" - @click="selectProject(project.id)" - > - {{ project.nameWithNamespace }} - </gl-dropdown-item> - <gl-dropdown-text - v-show="groupProjectsFlags.isLoading" - data-testid="dropdown-text-loading-icon" - > - <gl-loading-icon class="gl-mx-auto" size="sm" /> - </gl-dropdown-text> - <gl-dropdown-text - v-if="isFetchResultEmpty && !groupProjectsFlags.isLoading" - data-testid="empty-result-message" - > - <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> - </gl-dropdown-text> - <gl-intersection-observer v-if="hasNextPage" @appear="loadMoreProjects"> - <gl-loading-icon v-if="groupProjectsFlags.isLoadingMore" size="lg" /> - </gl-intersection-observer> - </gl-dropdown> + :searching="groupProjectsFlags.isLoading" + :search-placeholder="$options.i18n.searchPlaceholder" + :no-results-text="$options.i18n.emptySearchResult" + :infinite-scroll-loading="groupProjectsFlags.isLoadingMore" + @select="selectProject" + @search="onSearch" + @bottom-reached="loadMoreProjects" + /> </div> </template> diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue new file mode 100644 index 00000000000..a7787ae84bc --- /dev/null +++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_approved.vue @@ -0,0 +1,65 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import TargetLink from '../target_link.vue'; +import ResourceParentLink from '../resource_parent_link.vue'; +import ContributionEventBase from './contribution_event_base.vue'; + +export default { + name: 'ContributionEventApproved', + i18n: { + message: s__( + 'ContributionEvent|Approved merge request %{targetLink} in %{resourceParentLink}.', + ), + }, + components: { ContributionEventBase, GlSprintf, TargetLink, ResourceParentLink }, + props: { + /** + * Expected format + * { + * created_at: string; + * action: "approved" + * author: { + * id: number; + * username: string; + * name: string; + * state: string; + * avatar_url: string; + * web_url: string; + * }; + * target: { + * id: number; + * type: "MergeRequest" + * title: string; + * reference_link_text: string; + * web_url: string; + * }; + * resource_parent: { + * type: "project"; + * full_name: string; + * full_path: string; + * web_url: string; + * avatar_url: string; + * }; + * }; + */ + event: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <contribution-event-base :event="event" icon-name="approval-solid" icon-class="gl-text-green-500"> + <gl-sprintf :message="$options.i18n.message"> + <template #targetLink> + <target-link :event="event" /> + </template> + <template #resourceParentLink> + <resource-parent-link :event="event" /> + </template> + </gl-sprintf> + </contribution-event-base> +</template> diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue new file mode 100644 index 00000000000..93ac94a6f4f --- /dev/null +++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue @@ -0,0 +1,54 @@ +<script> +import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { GlAvatarLabeled, GlAvatarLink, GlIcon, TimeAgoTooltip }, + props: { + event: { + type: Object, + required: true, + }, + iconName: { + type: String, + required: true, + }, + iconClass: { + type: String, + required: false, + default: null, + }, + }, + computed: { + author() { + return this.event.author; + }, + authorUsername() { + return `@${this.author.username}`; + }, + }, +}; +</script> + +<template> + <li class="gl-mt-5 gl-pb-5 gl-border-b gl-relative"> + <time-ago-tooltip :time="event.created_at" class="gl-float-right gl-text-secondary" /> + <gl-avatar-link :href="author.web_url"> + <gl-avatar-labeled + :label="author.name" + :sub-label="authorUsername" + :src="author.avatar_url" + :size="32" + /> + </gl-avatar-link> + <div class="gl-pl-8 gl-mt-2" data-testid="event-body"> + <div class="gl-text-secondary"> + <gl-icon :class="iconClass" :name="iconName" /> + <slot></slot> + </div> + <div v-if="$scopedSlots['additional-info']" class="gl-mt-2"> + <slot name="additional-info"></slot> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/contribution_events/components/contribution_events.vue b/app/assets/javascripts/contribution_events/components/contribution_events.vue new file mode 100644 index 00000000000..41ec4f5692e --- /dev/null +++ b/app/assets/javascripts/contribution_events/components/contribution_events.vue @@ -0,0 +1,119 @@ +<script> +import EmptyComponent from '~/vue_shared/components/empty_component'; +import { EVENT_TYPE_APPROVED } from '../constants'; +import ContributionEventApproved from './contribution_event/contribution_event_approved.vue'; + +export default { + props: { + /** + * Expected format + * { + * created_at: string; + * action: + * | "created" + * | "updated" + * | "closed" + * | "reopened" + * | "pushed" + * | "commented" + * | "merged" + * | "joined" + * | "left" + * | "destroyed" + * | "expired" + * | "approved" + * | "private"; + * ref?: { + * type: "branch" | "tag"; + * count: number; + * name: string; + * path: string; + * is_new: boolean; + * is_removed: boolean; + * }; + * commit?: { + * truncated_sha: string; + * path: string; + * title: string; + * count: number; + * create_mr_path: string; + * from_truncated_sha?: string; + * to_truncated_sha?: string; + * compare_path?: string; + * }; + * author: { + * id: number; + * username: string; + * name: string; + * state: string; + * avatar_url: string; + * web_url: string; + * }; + * noteable?: { + * type: string; + * reference_link_text: string; + * web_url: string; + * first_line_in_markdown: string; + * }; + * target?: { + * id: number; + * type: + * | "Issue" + * | "Milestone" + * | "MergeRequest" + * | "Note" + * | "Project" + * | "Snippet" + * | "User" + * | "WikiPage::Meta" + * | "DesignManagement::Design"; + * title: string; + * issue_type?: + * | "issue" + * | "incident" + * | "test_case" + * | "requirement" + * | "task" + * | "objective" + * | "key_result"; + * reference_link_text?: string; + * web_url: string; + * }; + * resource_parent?: { + * type: "project" | "group"; + * full_name: string; + * full_path: string; + * web_url: string; + * avatar_url: string; + * }; + * }[]; + */ + events: { + type: Array, + required: true, + }, + }, + methods: { + eventComponent(action) { + switch (action) { + case EVENT_TYPE_APPROVED: + return ContributionEventApproved; + + default: + return EmptyComponent; + } + }, + }, +}; +</script> + +<template> + <ul class="gl-list-style-none gl-p-0"> + <component + :is="eventComponent(event.action)" + v-for="(event, index) in events" + :key="index" + :event="event" + /> + </ul> +</template> diff --git a/app/assets/javascripts/contribution_events/components/resource_parent_link.vue b/app/assets/javascripts/contribution_events/components/resource_parent_link.vue new file mode 100644 index 00000000000..5add9d788bb --- /dev/null +++ b/app/assets/javascripts/contribution_events/components/resource_parent_link.vue @@ -0,0 +1,22 @@ +<script> +import { GlLink } from '@gitlab/ui'; + +export default { + components: { GlLink }, + props: { + event: { + type: Object, + required: true, + }, + }, + computed: { + resourceParent() { + return this.event.resource_parent; + }, + }, +}; +</script> + +<template> + <gl-link :href="resourceParent.web_url">{{ resourceParent.full_name }}</gl-link> +</template> diff --git a/app/assets/javascripts/contribution_events/components/target_link.vue b/app/assets/javascripts/contribution_events/components/target_link.vue new file mode 100644 index 00000000000..a661121b2fb --- /dev/null +++ b/app/assets/javascripts/contribution_events/components/target_link.vue @@ -0,0 +1,31 @@ +<script> +import { GlLink } from '@gitlab/ui'; + +export default { + components: { GlLink }, + props: { + event: { + type: Object, + required: true, + }, + }, + computed: { + target() { + return this.event.target; + }, + targetLinkText() { + return this.target.reference_link_text; + }, + targetLinkAttributes() { + return { + href: this.target.web_url, + title: this.target.title, + }; + }, + }, +}; +</script> + +<template> + <gl-link v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link> +</template> diff --git a/app/assets/javascripts/contribution_events/constants.js b/app/assets/javascripts/contribution_events/constants.js new file mode 100644 index 00000000000..05f968e7bc4 --- /dev/null +++ b/app/assets/javascripts/contribution_events/constants.js @@ -0,0 +1,14 @@ +// From app/models/event.rb#L16 +export const EVENT_TYPE_CREATED = 'created'; +export const EVENT_TYPE_UPDATED = 'updated'; +export const EVENT_TYPE_CLOSED = 'closed'; +export const EVENT_TYPE_REOPENED = 'reopened'; +export const EVENT_TYPE_PUSHED = 'pushed'; +export const EVENT_TYPE_COMMENTED = 'commented'; +export const EVENT_TYPE_MERGED = 'merged'; +export const EVENT_TYPE_JOINED = 'joined'; +export const EVENT_TYPE_LEFT = 'left'; +export const EVENT_TYPE_DESTROYED = 'destroyed'; +export const EVENT_TYPE_EXPIRED = 'expired'; +export const EVENT_TYPE_APPROVED = 'approved'; +export const EVENT_TYPE_PRIVATE = 'private'; diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue index b492194d1cf..872e1d4269d 100644 --- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue +++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue @@ -1,18 +1,13 @@ <script> -import { GlDropdownItem, GlModalDirective } from '@gitlab/ui'; +import { GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui'; import { TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; import CsvExportModal from './csv_export_modal.vue'; import CsvImportModal from './csv_import_modal.vue'; export default { - i18n: { - exportAsCsvButtonText: __('Export as CSV'), - importCsvText: __('Import CSV'), - importFromJiraText: __('Import from Jira'), - }, components: { - GlDropdownItem, + GlDisclosureDropdownItem, CsvExportModal, CsvImportModal, }, @@ -48,6 +43,22 @@ export default { default: undefined, }, }, + data() { + return { + dropdownItems: { + exportAsCSV: { + text: __('Export as CSV'), + }, + importCSV: { + text: __('Import CSV'), + }, + importFromJIRA: { + text: __('Import from Jira'), + href: this.projectImportJiraPath, + }, + }, + }; + }, computed: { exportModalId() { return `${this.issuableType}-export-modal`; @@ -61,23 +72,25 @@ export default { <template> <ul class="gl-display-contents"> - <gl-dropdown-item + <gl-disclosure-dropdown-item v-if="showExportButton" v-gl-modal="exportModalId" + data-testid="export-as-csv-button" data-qa-selector="export_as_csv_button" - > - {{ $options.i18n.exportAsCsvButtonText }} - </gl-dropdown-item> - <gl-dropdown-item v-if="showImportButton" v-gl-modal="importModalId"> - {{ $options.i18n.importCsvText }} - </gl-dropdown-item> - <gl-dropdown-item + :item="dropdownItems.exportAsCSV" + /> + <gl-disclosure-dropdown-item + v-if="showImportButton" + v-gl-modal="importModalId" + data-testid="import-from-csv-button" + :item="dropdownItems.importCSV" + /> + <gl-disclosure-dropdown-item v-if="showImportButton && canEdit" - :href="projectImportJiraPath" + data-testid="import-from-jira-link" data-qa-selector="import_from_jira_link" - > - {{ $options.i18n.importFromJiraText }} - </gl-dropdown-item> + :item="dropdownItems.importFromJIRA" + /> <csv-export-modal v-if="showExportButton" diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue index c8b52609569..3f29fc66abb 100644 --- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue +++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlDropdown, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlButton, GlDisclosureDropdown, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue'; @@ -12,7 +12,7 @@ export default { components: { CsvImportExportButtons, GlButton, - GlDropdown, + GlDisclosureDropdown, GlEmptyState, GlLink, GlSprintf, @@ -77,17 +77,17 @@ export default { {{ $options.i18n.newIssueLabel }} </gl-button> - <gl-dropdown + <gl-disclosure-dropdown v-if="showCsvButtons" class="gl-w-full gl-sm-w-auto gl-sm-mr-3" - :text="$options.i18n.importIssues" + :toggle-text="$options.i18n.importIssues" data-qa-selector="import_issues_dropdown" > <csv-import-export-buttons :export-csv-path="exportCsvPathWithQuery" :issuable-count="currentTabCount" /> - </gl-dropdown> + </gl-disclosure-dropdown> <new-resource-dropdown v-if="showNewIssueDropdown" diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 2cb966cf1fd..83b0bcebe67 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -2,11 +2,10 @@ import { GlButton, GlButtonGroup, + GlDisclosureDropdown, + GlDisclosureDropdownGroup, GlFilteredSearchToken, GlTooltipDirective, - GlDropdown, - GlDropdownItem, - GlDropdownDivider, } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; @@ -70,6 +69,9 @@ import { defaultWorkItemTypes, i18n, ISSUE_REFERENCE, + ISSUES_GRID_VIEW_KEY, + ISSUES_LIST_VIEW_KEY, + ISSUES_VIEW_TYPE_KEY, MAX_LIST_SIZE, PARAM_FIRST_PAGE_SIZE, PARAM_LAST_PAGE_SIZE, @@ -80,9 +82,6 @@ import { RELATIVE_POSITION_ASC, UPDATED_DESC, urlSortParams, - ISSUES_VIEW_TYPE_KEY, - ISSUES_LIST_VIEW_KEY, - ISSUES_GRID_VIEW_KEY, } from '../constants'; import eventHub from '../eventhub'; import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql'; @@ -126,13 +125,12 @@ export default { ISSUES_LIST_VIEW_KEY, components: { CsvImportExportButtons, + GlDisclosureDropdown, + GlDisclosureDropdownGroup, EmptyStateWithAnyIssues, EmptyStateWithoutAnyIssues, GlButton, GlButtonGroup, - GlDropdown, - GlDropdownDivider, - GlDropdownItem, IssuableByEmail, IssuableList, IssueCardStatistics, @@ -205,6 +203,20 @@ export default { state: STATUS_OPEN, pageSize: DEFAULT_PAGE_SIZE, viewType: ISSUES_LIST_VIEW_KEY, + subscribeDropdownOptions: { + items: [ + { + text: i18n.rssLabel, + href: this.rssPath, + extraAttrs: { 'data-testid': 'subscribe-rss' }, + }, + { + text: i18n.calendarLabel, + href: this.calendarPath, + extraAttrs: { 'data-testid': 'subscribe-calendar' }, + }, + ], + }, }; }, apollo: { @@ -882,12 +894,12 @@ export default { :query-variables="newIssueDropdownQueryVariables" :extract-projects="extractProjects" /> - <gl-dropdown + <gl-disclosure-dropdown v-gl-tooltip.hover="$options.i18n.actionsLabel" category="tertiary" icon="ellipsis_v" no-caret - :text="$options.i18n.actionsLabel" + :toggle-text="$options.i18n.actionsLabel" text-sr-only data-qa-selector="issues_list_more_actions_dropdown" > @@ -896,16 +908,8 @@ export default { :export-csv-path="exportCsvPathWithQuery" :issuable-count="currentTabCount" /> - - <gl-dropdown-divider v-if="showCsvButtons" /> - - <gl-dropdown-item :href="rssPath"> - {{ $options.i18n.rssLabel }} - </gl-dropdown-item> - <gl-dropdown-item :href="calendarPath"> - {{ $options.i18n.calendarLabel }} - </gl-dropdown-item> - </gl-dropdown> + <gl-disclosure-dropdown-group :bordered="true" :group="subscribeDropdownOptions" /> + </gl-disclosure-dropdown> </template> <template #timeframe="{ issuable = {} }"> diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index 8c8cc7984b1..18dd3f4366c 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -22,7 +22,7 @@ export default { data-track-action="click_button" data-track-label="reply_comment_button" category="tertiary" - icon="comment" + icon="reply" :title="$options.i18n.buttonText" :aria-label="$options.i18n.buttonText" @click="$emit('startReplying')" diff --git a/app/assets/javascripts/profile/components/overview_tab.vue b/app/assets/javascripts/profile/components/overview_tab.vue index 21f8a2d3500..8cfa3fb3eea 100644 --- a/app/assets/javascripts/profile/components/overview_tab.vue +++ b/app/assets/javascripts/profile/components/overview_tab.vue @@ -1,16 +1,24 @@ <script> import { GlTab, GlLoadingIcon, GlLink } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import { createAlert } from '~/alert'; import { s__ } from '~/locale'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; +import ContributionEvents from '~/contribution_events/components/contribution_events.vue'; import ActivityCalendar from './activity_calendar.vue'; export default { i18n: { title: s__('UserProfile|Overview'), personalProjects: s__('UserProfile|Personal projects'), + activity: s__('UserProfile|Activity'), viewAll: s__('UserProfile|View all'), + eventsErrorMessage: s__( + 'UserProfile|An error occurred loading the activity. Please refresh the page to try again.', + ), }, - components: { GlTab, GlLoadingIcon, GlLink, ActivityCalendar, ProjectsList }, + components: { GlTab, GlLoadingIcon, GlLink, ActivityCalendar, ProjectsList, ContributionEvents }, + inject: ['userActivityPath'], props: { personalProjects: { type: Array, @@ -21,15 +29,44 @@ export default { required: true, }, }, + data() { + return { + events: [], + eventsLoading: false, + }; + }, + async mounted() { + this.eventsLoading = true; + + try { + const { data: events } = await axios.get(this.userActivityPath, { + params: { limit: 10 }, + }); + this.events = events; + } catch (error) { + createAlert({ message: this.$options.i18n.eventsErrorMessage, error, captureError: true }); + } finally { + this.eventsLoading = false; + } + }, }; </script> <template> <gl-tab :title="$options.i18n.title"> <activity-calendar /> - <div class="gl-mx-n3 gl-display-flex gl-flex-wrap"> - <div class="gl-px-3 gl-w-full gl-lg-w-half"></div> - <div class="gl-px-3 gl-w-full gl-lg-w-half" data-testid="personal-projects-section"> + <div class="gl-mx-n5 gl-display-flex gl-flex-wrap"> + <div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="activity-section"> + <div + class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid" + > + <h4 class="gl-flex-grow-1">{{ $options.i18n.activity }}</h4> + <gl-link href="">{{ $options.i18n.viewAll }}</gl-link> + </div> + <gl-loading-icon v-if="eventsLoading" class="gl-mt-5" size="md" /> + <contribution-events v-else :events="events" /> + </div> + <div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="personal-projects-section"> <div class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid" > diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js index 70c60c2d884..198ffdb434b 100644 --- a/app/assets/javascripts/profile/index.js +++ b/app/assets/javascripts/profile/index.js @@ -17,6 +17,7 @@ export const initProfileTabs = () => { followeesCount, followersCount, userCalendarPath, + userActivityPath, utcOffset, userId, snippetsEmptyState, @@ -34,6 +35,7 @@ export const initProfileTabs = () => { followeesCount: parseInt(followeesCount, 10), followersCount: parseInt(followersCount, 10), userCalendarPath, + userActivityPath, utcOffset, userId, snippetsEmptyState, diff --git a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue index 79f3faacac9..02adebc50af 100644 --- a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue +++ b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue @@ -105,7 +105,7 @@ export default { icon="dash" :aria-label="$options.i18n.removeItem" :title="$options.i18n.removeItem" - class="gl-align-self-center gl-p-1! gl-absolute gl-right-4" + class="gl-align-self-center gl-mr-2" data-testid="item-remove" @click.stop.prevent="handleItemRemove(item)" /> diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 90a040b97f7..0ee9db10ee2 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -147,14 +147,14 @@ export default { /> </slot> </div> - <div class="gl-pr-8 gl-text-gray-900 gl-truncate-end"> + <div class="gl-flex-grow-1 gl-text-gray-900 gl-truncate-end"> {{ item.title }} <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-truncate-end"> {{ item.subtitle }} </div> </div> <slot name="actions"></slot> - <span v-if="hasPill || isPinnable" class="gl-flex-grow-1 gl-text-right gl-mr-3 gl-relative"> + <span v-if="hasPill || isPinnable" class="gl-text-right gl-mr-3 gl-relative"> <gl-badge v-if="hasPill" size="sm" diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index dd6923d9fcd..69fce23ede4 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -1,9 +1,10 @@ <script> import { - GlDropdown, - GlDropdownSectionHeader, - GlFormInputGroup, GlButton, + GlDisclosureDropdown, + GlDisclosureDropdownItem, + GlFormGroup, + GlFormInputGroup, GlTooltipDirective, } from '@gitlab/ui'; import { getHTTPProtocol } from '~/lib/utils/url_utility'; @@ -11,8 +12,9 @@ import { __, sprintf } from '~/locale'; export default { components: { - GlDropdown, - GlDropdownSectionHeader, + GlDisclosureDropdown, + GlDisclosureDropdownItem, + GlFormGroup, GlFormInputGroup, GlButton, }, @@ -45,47 +47,45 @@ export default { }; </script> <template> - <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="confirm"> - <div class="pb-2 mx-1"> - <template v-if="sshLink"> - <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header> - - <div class="mx-3"> - <gl-form-input-group :value="sshLink" readonly select-on-click> - <template #append> - <gl-button - v-gl-tooltip.hover - :title="$options.copyURLTooltip" - :aria-label="$options.copyURLTooltip" - :data-clipboard-text="sshLink" - data-qa-selector="copy_ssh_url_button" - icon="copy-to-clipboard" - class="d-inline-flex" - /> - </template> - </gl-form-input-group> - </div> - </template> - - <template v-if="httpLink"> - <gl-dropdown-section-header>{{ httpLabel }}</gl-dropdown-section-header> - - <div class="mx-3"> - <gl-form-input-group :value="httpLink" readonly select-on-click> - <template #append> - <gl-button - v-gl-tooltip.hover - :title="$options.copyURLTooltip" - :aria-label="$options.copyURLTooltip" - :data-clipboard-text="httpLink" - data-qa-selector="copy_http_url_button" - icon="copy-to-clipboard" - class="d-inline-flex" - /> - </template> - </gl-form-input-group> - </div> - </template> - </div> - </gl-dropdown> + <gl-disclosure-dropdown + :toggle-text="$options.labels.defaultLabel" + category="primary" + variant="confirm" + placement="right" + > + <gl-disclosure-dropdown-item v-if="sshLink"> + <gl-form-group :label="$options.labels.ssh" class="gl-px-3 gl-my-3"> + <gl-form-input-group :value="sshLink" readonly select-on-click> + <template #append> + <gl-button + v-gl-tooltip.hover + :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" + :data-clipboard-text="sshLink" + data-qa-selector="copy_ssh_url_button" + icon="copy-to-clipboard" + class="gl-display-inline-flex" + /> + </template> + </gl-form-input-group> + </gl-form-group> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="httpLink"> + <gl-form-group :label="httpLabel" class="gl-px-3 gl-mb-3"> + <gl-form-input-group :value="httpLink" readonly select-on-click> + <template #append> + <gl-button + v-gl-tooltip.hover + :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" + :data-clipboard-text="httpLink" + data-qa-selector="copy_http_url_button" + icon="copy-to-clipboard" + class="gl-display-inline-flex" + /> + </template> + </gl-form-input-group> + </gl-form-group> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown> </template> diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index ab7f9bb4927..10d0d03e56d 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -28,7 +28,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :cancel_auto_stop] before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? } - before_action :set_kas_cookie, only: [:index], if: -> { current_user } + before_action :set_kas_cookie, only: [:index], if: -> { current_user && request.format.html? } after_action :expire_etag_cache, only: [:cancel_auto_stop] track_event :index, :folder, :show, :new, :edit, :create, :update, :stop, :cancel_auto_stop, :terminal, diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 108ee65bffd..c8002c437a9 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -187,6 +187,7 @@ module UsersHelper followees_count: user.followees.count, followers_count: user.followers.count, user_calendar_path: user_calendar_path(user, :json), + user_activity_path: user_activity_path(user, :json), utc_offset: local_timezone_instance(user.timezone).now.utc_offset, user_id: user.id, snippets_empty_state: image_path('illustrations/empty-state/empty-snippets-md.svg') diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a823b8a93fb..6cb95d665cc 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -695,13 +695,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, if: :update_runner_versions_enabled? - validates :unconfirmed_users_delete_after_days, - numericality: { only_integer: true, greater_than: 0 }, - if: :delete_unconfirmed_users - - validates :delete_unconfirmed_users, - inclusion: { in: [true, false], message: N_('must be a boolean value') } - validates :inactive_projects_min_size_mb, numericality: { only_integer: true, greater_than_or_equal_to: 0 } diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 1094a131e68..c0ffbb401f6 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -22,9 +22,19 @@ module Ci # Run the process only if we can obtain an exclusive lease; returns nil if lease is unavailable success = try_obtain_lease { process! } - # Re-schedule if we need further processing - if success && pipeline.needs_processing? - PipelineProcessWorker.perform_async(pipeline.id) + if success + if ::Feature.enabled?(:ci_reset_skipped_jobs_in_atomic_processing, project) + # If any jobs changed from stopped to alive status during pipeline processing, we must + # re-reset their dependent jobs; see https://gitlab.com/gitlab-org/gitlab/-/issues/388539. + new_alive_jobs.group_by(&:user).each do |user, jobs| + log_running_reset_skipped_jobs_service(jobs) + + ResetSkippedJobsService.new(project, user).execute(jobs) + end + end + + # Re-schedule if we need further processing + PipelineProcessWorker.perform_async(pipeline.id) if pipeline.needs_processing? end success @@ -105,6 +115,25 @@ module Ci end end + # Gets the jobs that changed from stopped to alive status since the initial status collection + # was evaluated. We determine this by checking if their current status is no longer stopped. + def new_alive_jobs + initial_stopped_job_names = @collection.stopped_job_names + + return [] if initial_stopped_job_names.empty? + + new_collection = AtomicProcessingService::StatusCollection.new(pipeline) + new_alive_job_names = initial_stopped_job_names - new_collection.stopped_job_names + + return [] if new_alive_job_names.empty? + + pipeline + .current_jobs + .by_name(new_alive_job_names) + .preload(:user) # rubocop: disable CodeReuse/ActiveRecord + .to_a + end + def project pipeline.project end @@ -116,6 +145,17 @@ module Ci def lease_timeout DEFAULT_LEASE_TIMEOUT end + + def log_running_reset_skipped_jobs_service(jobs) + Gitlab::AppJsonLogger.info( + class: self.class.name.to_s, + message: 'Running ResetSkippedJobsService on new alive jobs', + project_id: project.id, + pipeline_id: pipeline.id, + user_id: jobs.first.user.id, + jobs_count: jobs.count + ) + end end end end diff --git a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb index 85646b79254..9a53c6d8fc1 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb @@ -67,6 +67,11 @@ module Ci all_jobs.lazy.reject { |job| job[:processed] } end + # This method returns the names of jobs that have a stopped status + def stopped_job_names + all_jobs.select { |job| job[:status].in?(Ci::HasStatus::STOPPED_STATUSES) }.pluck(:name) # rubocop: disable CodeReuse/ActiveRecord + end + private # We use these columns to perform an efficient calculation of a status |