diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-19 18:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-19 18:09:36 +0300 |
commit | 8bb837c4d180720d4d923ef2e7bd2c9a46ca97a0 (patch) | |
tree | 7dcb166661ba29fb6cd5935f0db34eee6c935388 /app | |
parent | eef2437c0a359ec3437d31d1b1ea959e54c71458 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
9 files changed, 255 insertions, 66 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index 182090e64f9..0151dbb0bf7 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -24,6 +24,7 @@ import { import { severityLevel, severityLevelVariant, errorStatus } from '../constants'; import Stacktrace from './stacktrace.vue'; import ErrorDetailsInfo from './error_details_info.vue'; +import TimelineChart from './timeline_chart.vue'; const SENTRY_TIMEOUT = 10000; @@ -42,6 +43,7 @@ export default { GlDropdownDivider, TimeAgoTooltip, ErrorDetailsInfo, + TimelineChart, }, props: { issueUpdatePath: { @@ -375,6 +377,11 @@ export default { <error-details-info :error="error" /> + <div v-if="error.frequency" class="gl-mt-8"> + <h3>{{ __('Last 24 hours') }}</h3> + <timeline-chart :timeline-data="error.frequency" :height="200" /> + </div> + <div v-if="loadingStacktrace" class="gl-py-5"> <gl-loading-icon size="lg" /> </div> diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index e3784cc8b92..0c9a98f3b33 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -30,10 +30,12 @@ import { } from '../events_tracking'; import { I18N_ERROR_TRACKING_LIST } from '../constants'; import ErrorTrackingActions from './error_tracking_actions.vue'; +import TimelineChart from './timeline_chart.vue'; const isValidErrorId = (errorId) => { return /^[0-9]+$/.test(errorId); }; +export const tableDataClass = 'gl-display-flex gl-md-display-table-cell gl-align-items-center'; export default { FIRST_PAGE: 1, PREV_PAGE: 1, @@ -43,30 +45,37 @@ export default { { key: 'error', label: __('Error'), - thClass: 'w-60p', + thClass: 'gl-w-40p', + tdClass: `${tableDataClass}`, + }, + { + key: 'timeline', + label: __('Timeline'), + thClass: 'gl-text-center gl-w-20p', + tdClass: `${tableDataClass} gl-text-center`, }, { key: 'events', label: __('Events'), - thClass: 'gl-text-right', - tdClass: 'gl-text-right', + thClass: 'gl-text-center gl-w-10p', + tdClass: `${tableDataClass} gl-text-center`, }, { key: 'users', label: __('Users'), - thClass: 'gl-text-right', - tdClass: 'gl-text-right', + thClass: 'gl-text-center gl-w-10p', + tdClass: `${tableDataClass} gl-text-center`, }, { key: 'lastSeen', label: __('Last seen'), - thClass: 'gl-w-15p', - tdClass: 'gl-text-left', + thClass: 'gl-text-center gl-w-10p', + tdClass: `${tableDataClass} gl-text-center`, }, { key: 'status', label: '', - tdClass: 'gl-text-center', + tdClass: `${tableDataClass}`, }, ], statusFilters: { @@ -95,6 +104,7 @@ export default { GlPagination, TimeAgo, ErrorTrackingActions, + TimelineChart, }, directives: { GlTooltip: GlTooltipDirective, @@ -438,6 +448,14 @@ export default { </div> </template> + <template #cell(timeline)="errors"> + <timeline-chart + v-if="errors.item.frequency" + :timeline-data="errors.item.frequency" + :height="70" + /> + </template> + <template #cell(events)="errors"> {{ errors.item.count }} </template> diff --git a/app/assets/javascripts/error_tracking/components/timeline_chart.vue b/app/assets/javascripts/error_tracking/components/timeline_chart.vue new file mode 100644 index 00000000000..51e0c900e4b --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/timeline_chart.vue @@ -0,0 +1,129 @@ +<script> +import { GlChart } from '@gitlab/ui/dist/charts'; +import { dataVizBlue500 } from '@gitlab/ui/scss_to_js/scss_variables'; +import { hexToRgba } from '@gitlab/ui/dist/utils/utils'; +import { isNumber } from 'lodash'; +import { formatDate } from '~/lib/utils/datetime/date_format_utility'; +import { logError } from '~/lib/logger'; + +function parseTimelineData(timelineData) { + const xData = []; + const yData = []; + const invalidDataPoints = []; + timelineData.forEach((f) => { + let rawDate; + let count; + + if (Array.isArray(f)) { + [rawDate, count] = f; + } else if (f.count !== undefined && f.time !== undefined) { + rawDate = f.time; + count = f.count; + } + if (rawDate !== undefined && count !== undefined) { + // dates/timestamps are in seconds + const date = isNumber(rawDate) ? rawDate * 1000 : rawDate; + xData.push(formatDate(date)); + yData.push(count); + } else { + invalidDataPoints.push(f); + } + }); + if (invalidDataPoints.length > 0) { + // only log up to 5 invalid data points to reduce log size + logError(`Found invalid data points ${invalidDataPoints.slice(0, 5)}`); + } + return { xData, yData }; +} + +export default { + components: { + GlChart, + }, + props: { + timelineData: { + /** + * Array items can be: + * touples: [a_date: string | number, a_count: number] + * objects: {time: a_date, count: a_count}: {time: string | number, count: number} + * + * Dates can either be string or number/timestamp. + * When dates are timestamps, they are expected in seconds. + * + */ + type: Array, + required: true, + validator(value) { + for (const item of value) { + if (Array.isArray(item)) { + if (item.length !== 2 || !isNumber(item[1])) { + return false; + } + } else if (typeof item === 'object') { + if (!('time' in item) || !('count' in item)) { + return false; + } + } else { + return false; + } + } + return true; + }, + }, + height: { + type: Number, + required: true, + }, + }, + computed: { + chartOptions() { + if (!this.timelineData) { + return {}; + } + const { xData, yData } = parseTimelineData(this.timelineData); + + return { + xAxis: { + type: 'category', + data: xData, + show: true, + axisTick: { + show: false, + }, + axisLabel: { + show: false, + }, + axisLine: { + show: true, + lineStyle: { + width: 1, + color: '#ececec', + }, + }, + }, + yAxis: { + type: 'value', + show: false, + }, + series: [ + { + data: yData, + type: 'bar', + itemStyle: { color: hexToRgba(dataVizBlue500, 0.5) }, + }, + ], + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + }, + }; + }, + }, +}; +</script> + +<template> + <gl-chart v-if="timelineData" :options="chartOptions" :height="height" /> +</template> diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql index dd21b0f9c92..5745491c32d 100644 --- a/app/assets/javascripts/error_tracking/queries/details.query.graphql +++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql @@ -20,6 +20,10 @@ query errorDetails($fullPath: ID!, $errorId: GitlabErrorTrackingDetailedErrorID! externalUrl externalBaseUrl firstReleaseVersion + frequency { + count + time + } lastReleaseVersion gitlabCommit gitlabCommitPath diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue index a85bb09e17b..4571c4172e5 100644 --- a/app/assets/javascripts/members/components/table/role_dropdown.vue +++ b/app/assets/javascripts/members/components/table/role_dropdown.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { mapActions } from 'vuex'; import * as Sentry from '@sentry/browser'; @@ -9,10 +9,9 @@ import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_conf export default { name: 'RoleDropdown', components: { - GlDropdown, - GlDropdownItem, - LdapDropdownItem: () => - import('ee_component/members/components/action_dropdowns/ldap_dropdown_item.vue'), + GlCollapsibleListbox, + LdapDropdownFooter: () => + import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'), }, inject: ['namespace', 'group'], props: { @@ -29,23 +28,22 @@ export default { return { isDesktop: false, busy: false, + selectedRoleValue: this.member.accessLevel.integerValue, }; }, computed: { disabled() { return this.permissions.canOverride && !this.member.isOverridden; }, + dropdownItems() { + return Object.entries(this.member.validRoles).map(([name, value]) => ({ + value, + text: name, + })); + }, }, mounted() { this.isDesktop = bp.isDesktop(); - - // Bootstrap Vue and GlDropdown to not support adding attributes to the dropdown toggle - // This can be changed once https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1060 is implemented - const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle'); - - if (dropdownToggle) { - dropdownToggle.dataset.qaSelector = 'access_level_dropdown'; - } }, methods: { ...mapActions({ @@ -63,7 +61,7 @@ export default { memberType: this.namespace, }); }, - async handleSelect(newRoleValue, newRoleName) { + async handleSelect(newRoleValue) { const currentRoleValue = this.member.accessLevel.integerValue; if (newRoleValue === currentRoleValue) { return; @@ -71,6 +69,7 @@ export default { this.busy = true; + const { text: newRoleName } = this.dropdownItems.find((item) => item.value === newRoleValue); const confirmed = await this.handleOverageConfirm( currentRoleValue, newRoleValue, @@ -99,27 +98,25 @@ export default { </script> <template> - <gl-dropdown - ref="glDropdown" - :right="!isDesktop" - :text="member.accessLevel.stringValue" + <gl-collapsible-listbox + v-model="selectedRoleValue" + :placement="isDesktop ? 'left' : 'right'" + :toggle-text="member.accessLevel.stringValue" :header-text="__('Change role')" :disabled="disabled" :loading="busy" + data-qa-selector="access_level_dropdown" + :items="dropdownItems" + @select="handleSelect" > - <gl-dropdown-item - v-for="(value, name) in member.validRoles" - :key="value" - is-check-item - :is-checked="value === member.accessLevel.integerValue" - data-qa-selector="access_level_link" - @click="handleSelect(value, name)" - > - {{ name }} - </gl-dropdown-item> - <ldap-dropdown-item - v-if="permissions.canOverride && member.isOverridden" - :member-id="member.id" - /> - </gl-dropdown> + <template #list-item="{ item }"> + <span data-qa-selector="access_level_link">{{ item.text }}</span> + </template> + <template #footer> + <ldap-dropdown-footer + v-if="permissions.canOverride && member.isOverridden" + :member-id="member.id" + /> + </template> + </gl-collapsible-listbox> </template> diff --git a/app/assets/javascripts/profile/components/user_achievements.vue b/app/assets/javascripts/profile/components/user_achievements.vue index fd42b64f4c5..13a1b797a83 100644 --- a/app/assets/javascripts/profile/components/user_achievements.vue +++ b/app/assets/javascripts/profile/components/user_achievements.vue @@ -1,6 +1,7 @@ <script> -import { GlPopover, GlSprintf } from '@gitlab/ui'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { GlAvatar, GlBadge, GlPopover, GlSprintf } from '@gitlab/ui'; +import { groupBy } from 'lodash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import { s__ } from '~/locale'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -8,7 +9,7 @@ import getUserAchievements from './graphql/get_user_achievements.query.graphql'; export default { name: 'UserAchievements', - components: { GlPopover, GlSprintf }, + components: { GlAvatar, GlBadge, GlPopover, GlSprintf }, mixins: [timeagoMixin], inject: ['rootUrl', 'userId'], apollo: { @@ -29,25 +30,39 @@ export default { }, methods: { processNodes(nodes) { - return nodes.slice(0, 3).map(({ achievement, createdAt, achievement: { namespace } }) => { - return { - id: `user-achievement-${getIdFromGraphQLId(achievement.id)}`, - name: achievement.name, - timeAgo: this.timeFormatted(createdAt), - avatarUrl: achievement.avatarUrl || gon.gitlab_logo, - description: achievement.description, - namespace: namespace && { - fullPath: namespace.fullPath, - webUrl: this.rootUrl + namespace.fullPath, - }, - }; - }); + return Object.entries(groupBy(nodes, 'achievement.id')) + .slice(0, 3) + .map(([id, values]) => { + const { + achievement: { name, avatarUrl, description, namespace }, + createdAt, + } = values[0]; + const count = values.length; + return { + id: `user-achievement-${id}`, + name, + timeAgo: this.timeFormatted(createdAt), + avatarUrl: avatarUrl || gon.gitlab_logo, + description, + namespace: namespace && { + fullPath: namespace.fullPath, + webUrl: this.rootUrl + namespace.fullPath, + }, + count, + }; + }); }, achievementAwardedMessage(userAchievement) { return userAchievement.namespace ? this.$options.i18n.awardedBy : this.$options.i18n.awardedByUnknownNamespace; }, + showCountBadge(count) { + return count > 1; + }, + getCountBadge(count) { + return `${count}x`; + }, }, i18n: { awardedBy: s__('Achievements|Awarded %{timeAgo} by %{namespace}'), @@ -61,18 +76,28 @@ export default { <div v-for="userAchievement in userAchievements" :key="userAchievement.id" - class="gl-display-inline-block" + class="gl-display-inline-block gl-vertical-align-top" data-testid="user-achievement" > - <img + <gl-avatar :id="userAchievement.id" :src="userAchievement.avatarUrl" - :alt="''" + :size="32" tabindex="0" - class="gl-avatar gl-avatar-s32 gl-mx-2" + shape="rect" + class="gl-mx-2" /> - <gl-popover triggers="hover focus" placement="top" :target="userAchievement.id"> - <div class="gl-font-weight-bold">{{ userAchievement.name }}</div> + <br /> + <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{ + getCountBadge(userAchievement.count) + }}</gl-badge> + <gl-popover :target="userAchievement.id"> + <div> + <span class="gl-font-weight-bold">{{ userAchievement.name }}</span> + <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{ + getCountBadge(userAchievement.count) + }}</gl-badge> + </div> <div> <gl-sprintf :message="achievementAwardedMessage(userAchievement)"> <template #timeAgo> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index 330db7ff2ee..c330eccb186 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -137,8 +137,8 @@ export default { isProjectArchived() { return this.workItem?.project?.archived; }, - canUpdate() { - return this.workItem?.userPermissions?.updateWorkItem; + canCreateNote() { + return this.workItem?.userPermissions?.createNote; }, workItemState() { return this.workItem?.state; @@ -243,7 +243,7 @@ export default { <li :class="timelineEntryClass"> <work-item-note-signed-out v-if="!signedIn" /> <work-item-comment-locked - v-else-if="!canUpdate" + v-else-if="!canCreateNote" :work-item-type="workItemType" :is-project-archived="isProjectArchived" /> diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql index 20a8d127e7d..1ae5617f04d 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -32,6 +32,7 @@ fragment WorkItem on WorkItem { updateWorkItem adminParentLink setWorkItemMetadata + createNote } widgets { ...WorkItemWidgets diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index d6740cdf1ac..3a5e8d09759 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -169,7 +169,15 @@ module MergeRequests @outdate_service ||= Suggestions::OutdateService.new end + def abort_auto_merges?(merge_request) + return true unless Feature.enabled?(:fix_interrupted_mwps, @project) + + merge_request.merge_params.with_indifferent_access[:sha] != @push.newrev + end + def abort_auto_merges(merge_request) + return unless abort_auto_merges?(merge_request) + abort_auto_merge(merge_request, 'source branch was updated') end |