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>2023-06-19 18:09:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-19 18:09:36 +0300
commit8bb837c4d180720d4d923ef2e7bd2c9a46ca97a0 (patch)
tree7dcb166661ba29fb6cd5935f0db34eee6c935388 /app
parenteef2437c0a359ec3437d31d1b1ea959e54c71458 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue7
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue34
-rw-r--r--app/assets/javascripts/error_tracking/components/timeline_chart.vue129
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql4
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue63
-rw-r--r--app/assets/javascripts/profile/components/user_achievements.vue69
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/services/merge_requests/refresh_service.rb8
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