diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-31 21:10:00 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-31 21:10:00 +0300 |
commit | 22dde36e800253350e5fa1d902f191a7f64bc6e9 (patch) | |
tree | 3b53aee41daed5efa0674f9ee2da83e17ef4e676 /app/assets | |
parent | 6f18a8d0b00eae84d262dff137fddd9639f3c52a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
13 files changed, 284 insertions, 68 deletions
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index c62736d55a8..a82633035b5 100644 --- a/app/assets/javascripts/analytics/shared/constants.js +++ b/app/assets/javascripts/analytics/shared/constants.js @@ -1,5 +1,6 @@ import { masks } from '~/lib/dateformat'; import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; export const DATE_RANGE_LIMIT = 180; export const PROJECTS_PER_PAGE = 50; @@ -12,8 +13,101 @@ export const dateFormats = { month: 'mmmm', }; -// Some content is duplicated due to backward compatibility. -// It will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/350614 in 14.9 +export const KEY_METRICS = { + LEAD_TIME: 'lead_time', + CYCLE_TIME: 'cycle_time', + ISSUES: 'issues', + COMMITS: 'commits', + DEPLOYS: 'deploys', +}; + +export const DORA_METRICS = { + DEPLOYMENT_FREQUENCY: 'deployment_frequency', + LEAD_TIME_FOR_CHANGES: 'lead_time_for_changes', + TIME_TO_RESTORE_SERVICE: 'time_to_restore_service', + CHANGE_FAILURE_RATE: 'change_failure_rate', +}; + +export const VSA_METRICS_GROUPS = [ + { + key: 'key_metrics', + title: s__('ValueStreamAnalytics|Key metrics'), + keys: Object.values(KEY_METRICS), + }, + { + key: 'dora_metrics', + title: s__('ValueStreamAnalytics|DORA metrics'), + keys: Object.values(DORA_METRICS), + }, +]; + +export const METRIC_TOOLTIPS = { + [DORA_METRICS.DEPLOYMENT_FREQUENCY]: { + description: s__( + 'ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users.', + ), + groupLink: '-/analytics/ci_cd?tab=deployment-frequency', + projectLink: '-/pipelines/charts?chart=deployment-frequency', + docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'deployment-frequency' }), + }, + [DORA_METRICS.LEAD_TIME_FOR_CHANGES]: { + description: s__( + 'ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines.', + ), + groupLink: '-/analytics/ci_cd?tab=lead-time', + projectLink: '-/pipelines/charts?chart=lead-time', + docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'lead-time-for-changes' }), + }, + [DORA_METRICS.TIME_TO_RESTORE_SERVICE]: { + description: s__( + 'ValueStreamAnalytics|The time it takes an organization to recover from a failure in production.', + ), + groupLink: '-/analytics/ci_cd?tab=time-to-restore-service', + projectLink: '-/pipelines/charts?chart=time-to-restore-service', + docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'time-to-restore-service' }), + }, + [DORA_METRICS.CHANGE_FAILURE_RATE]: { + description: s__( + 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.', + ), + groupLink: '-/analytics/ci_cd?tab=change-failure-rate', + projectLink: '-/pipelines/charts?chart=change-failure-rate', + docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'change-failure-rate' }), + }, + [KEY_METRICS.LEAD_TIME]: { + description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), + groupLink: '-/analytics/value_stream_analytics', + projectLink: '-/value_stream_analytics', + docsLink: helpPagePath('user/analytics/value_stream_analytics', { + anchor: 'view-the-lead-time-and-cycle-time-for-issues', + }), + }, + [KEY_METRICS.CYCLE_TIME]: { + description: s__( + "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", + ), + groupLink: '-/analytics/value_stream_analytics', + projectLink: '-/value_stream_analytics', + docsLink: helpPagePath('user/analytics/value_stream_analytics', { + anchor: 'view-the-lead-time-and-cycle-time-for-issues', + }), + }, + [KEY_METRICS.ISSUES]: { + description: s__('ValueStreamAnalytics|Number of new issues created.'), + groupLink: '-/issues_analytics', + projectLink: '-/analytics/issues_analytics', + docsLink: helpPagePath('user/analytics/issue_analytics'), + }, + [KEY_METRICS.DEPLOYS]: { + description: s__('ValueStreamAnalytics|Total number of deploys to production.'), + groupLink: '-/analytics/productivity_analytics', + projectLink: '-/analytics/merge_request_analytics', + docsLink: helpPagePath('user/analytics/merge_request_analytics'), + }, +}; + +// TODO: Remove this once the migration to METRIC_TOOLTIPS is complete +// https://gitlab.com/gitlab-org/gitlab/-/issues/388067 export const METRICS_POPOVER_CONTENT = { lead_time: { description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), @@ -47,19 +141,3 @@ export const METRICS_POPOVER_CONTENT = { ), }, }; - -const KEY_METRICS_TITLE = s__('ValueStreamAnalytics|Key metrics'); -const KEY_METRICS_KEYS = ['lead_time', 'cycle_time', 'issues', 'commits', 'deploys']; - -const DORA_METRICS_TITLE = s__('ValueStreamAnalytics|DORA metrics'); -const DORA_METRICS_KEYS = [ - 'deployment_frequency', - 'lead_time_for_changes', - 'time_to_restore_service', - 'change_failure_rate', -]; - -export const VSA_METRICS_GROUPS = [ - { key: 'key_metrics', title: KEY_METRICS_TITLE, keys: KEY_METRICS_KEYS }, - { key: 'dora_metrics', title: DORA_METRICS_TITLE, keys: DORA_METRICS_KEYS }, -]; diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js index 7352be0dbd5..12fdb2e2981 100644 --- a/app/assets/javascripts/behaviors/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts.js @@ -28,7 +28,10 @@ export default function initPageShortcuts() { // TODO: replace this whitelist with something more automated/maintainable if (page && !pagesWithCustomShortcuts.includes(page)) { import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts') - .then(({ default: Shortcuts }) => new Shortcuts()) + .then(({ default: Shortcuts }) => { + const shortcuts = new Shortcuts(); + window.toggleShortcutsHelp = shortcuts.onToggleHelp; + }) .catch(() => {}); } return false; diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index b8719d0cd4d..430498e7194 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -1,6 +1,5 @@ <script> import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { snakeCase } from 'lodash'; import SafeHtml from '~/vue_shared/directives/safe_html'; import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; @@ -61,7 +60,7 @@ export default { return highlight(this.itemName, this.matcher); }, itemTrackingLabel() { - return `${this.dropdownType}_dropdown_frequent_items_list_item_${snakeCase(this.itemName)}`; + return `${this.dropdownType}_dropdown_frequent_items_list_item`; }, }, methods: { diff --git a/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js index 095da60a583..9c891bcfc9e 100644 --- a/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js @@ -1,9 +1,9 @@ /* eslint-disable class-methods-use-this, no-new */ - import $ from 'jquery'; import issuableEventHub from '~/issues/list/eventhub'; import LabelsSelect from '~/labels/labels_select'; import { + mountAssigneesDropdown, mountMilestoneDropdown, mountMoveIssuesButton, mountStatusDropdown, @@ -64,6 +64,7 @@ export default class IssuableBulkUpdateSidebar { mountMoveIssuesButton(); mountStatusDropdown(); mountSubscriptionsDropdown(); + mountAssigneesDropdown(); // Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy // the import/no-unresolved lint rule when FOSS_ONLY=1, even though at 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 22e9dd6a5c5..28908478e02 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -592,9 +592,6 @@ export default { const bulkUpdateSidebar = await import('~/issuable'); bulkUpdateSidebar.initBulkUpdateSidebar('issuable_'); - const UsersSelect = (await import('~/users_select')).default; - new UsersSelect(); // eslint-disable-line no-new - this.hasInitBulkEdit = true; } diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 90c1b31286a..b8138f34d45 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -56,7 +56,11 @@ function initDeferred() { if (!appEl) return; setNotification(appEl); - document.querySelector('.js-whats-new-trigger').addEventListener('click', () => { + + const triggerEl = document.querySelector('.js-whats-new-trigger'); + if (!triggerEl) return; + + triggerEl.addEventListener('click', () => { import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new') .then(({ default: initWhatsNew }) => { initWhatsNew(appEl); diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index facc07e8ce5..bef4c6af6ab 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,21 +1,22 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { IssuableType } from '~/issues/constants'; import { gqlClient } from '~/issues/list/graphql'; import { - isInIssuePage, isInDesignPage, isInIncidentPage, + isInIssuePage, isInMRPage, parseBoolean, } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import { apolloProvider } from '~/graphql_shared/issuable_client'; import Translate from '~/vue_shared/translate'; +import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue'; @@ -725,6 +726,70 @@ export function mountMoveIssueButton() { }); } +export function mountAssigneesDropdown() { + const el = document.querySelector('.js-assignee-dropdown'); + const assigneeIdsInput = document.querySelector('.js-assignee-ids-input'); + + if (!el || !assigneeIdsInput) { + return null; + } + + const { fullPath } = el.dataset; + const currentUser = { + id: gon?.current_user_id, + username: gon?.current_username, + name: gon?.current_user_fullname, + avatarUrl: gon?.current_user_avatar_url, + }; + + return new Vue({ + el, + apolloProvider, + data() { + return { + selectedUserName: '', + value: [], + }; + }, + methods: { + onSelectedUnassigned() { + assigneeIdsInput.value = 0; + this.value = []; + this.selectedUserName = __('Unassigned'); + }, + onSelected(selected) { + assigneeIdsInput.value = selected.map((user) => getIdFromGraphQLId(user.id)); + this.value = selected; + this.selectedUserName = selected.map((user) => user.name).join(', '); + }, + }, + render(h) { + const component = this; + + return h(UserSelect, { + props: { + text: component.selectedUserName || __('Select assignee'), + headerText: __('Assign to'), + fullPath, + currentUser, + value: component.value, + }, + on: { + input(selected) { + if (!selected.length) { + component.onSelectedUnassigned(); + return; + } + + component.onSelected(selected); + }, + }, + class: 'gl-w-full', + }); + }, + }); +} + const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget; diff --git a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue deleted file mode 100644 index fea29458f45..00000000000 --- a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue +++ /dev/null @@ -1,24 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default { - components: { - GlIcon, - }, - i18n: { - help: __('Help'), - new: __('New'), - }, -}; -</script> - -<template> - <div class="bottom-links gl-p-3"> - <a href="#" class="gl-text-black-normal" - ><gl-icon name="question-o" class="gl-mr-3 gl-text-gray-300 gl-text-black-normal!" />{{ - $options.i18n.help - }}</a - > - </div> -</template> diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue new file mode 100644 index 00000000000..6c82a4bbf86 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -0,0 +1,88 @@ +<script> +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; +import { __ } from '~/locale'; + +export default { + components: { + GlDisclosureDropdown, + }, + i18n: { + help: __('Help'), + support: __('Support'), + docs: __('GitLab documentation'), + plans: __('Compare GitLab plans'), + forum: __('Community forum'), + contribute: __('Contribute to GitLab'), + feedback: __('Provide feedback'), + shortcuts: __('Keyboard shortcuts'), + whatsnew: __("What's new"), + }, + props: { + sidebarData: { + type: Object, + required: true, + }, + }, + data() { + return { + items: [ + { + items: [ + { text: this.$options.i18n.help, href: helpPagePath() }, + { text: this.$options.i18n.support, href: this.sidebarData.support_path }, + { text: this.$options.i18n.docs, href: 'https://docs.gitlab.com' }, + { text: this.$options.i18n.plans, href: `${PROMO_URL}/pricing` }, + { text: this.$options.i18n.forum, href: 'https://forum.gitlab.com/' }, + { + text: this.$options.i18n.contribute, + href: helpPagePath('', { anchor: 'contributing-to-gitlab' }), + }, + { text: this.$options.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' }, + ], + }, + { + items: [ + { text: this.$options.i18n.shortcuts, action: this.showKeyboardShortcuts }, + this.sidebarData.display_whats_new && { + text: this.$options.i18n.whatsnew, + action: this.showWhatsNew, + }, + ].filter(Boolean), + }, + ], + }; + }, + methods: { + showKeyboardShortcuts() { + this.$refs.dropdown.close(); + window?.toggleShortcutsHelp(); + }, + async showWhatsNew() { + this.$refs.dropdown.close(); + if (!this.toggleWhatsNewDrawer) { + const appEl = document.getElementById('whats-new-app'); + const { default: toggleWhatsNewDrawer } = await import( + /* webpackChunkName: 'whatsNewApp' */ '~/whats_new' + ); + this.toggleWhatsNewDrawer = toggleWhatsNewDrawer; + this.toggleWhatsNewDrawer(appEl); + } else { + this.toggleWhatsNewDrawer(); + } + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown + ref="dropdown" + icon="question-o" + :items="items" + :toggle-text="$options.i18n.help" + category="tertiary" + no-caret + /> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index 78d761e9a59..c4b769dcf24 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -4,7 +4,7 @@ import { context } from '../mock_data'; import UserBar from './user_bar.vue'; import ContextSwitcherToggle from './context_switcher_toggle.vue'; import ContextSwitcher from './context_switcher.vue'; -import BottomBar from './bottom_bar.vue'; +import HelpCenter from './help_center.vue'; export default { context, @@ -13,7 +13,7 @@ export default { UserBar, ContextSwitcherToggle, ContextSwitcher, - BottomBar, + HelpCenter, }, props: { sidebarData: { @@ -32,7 +32,7 @@ export default { <template> <aside id="super-sidebar" - class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08 gl-z-index-9999" + class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08" data-testid="super-sidebar" > <user-bar :sidebar-data="sidebarData" /> @@ -43,8 +43,8 @@ export default { <context-switcher /> </gl-collapse> </div> - <div class="gl-px-3"> - <bottom-bar /> + <div class="gl-p-3"> + <help-center :sidebar-data="sidebarData" /> </div> </div> </aside> diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue index 86a99b8f0ed..0c8cd7fd95c 100644 --- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue +++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue @@ -2,11 +2,11 @@ import { debounce } from 'lodash'; import { GlDropdown, - GlDropdownForm, GlDropdownDivider, + GlDropdownForm, GlDropdownItem, - GlSearchBoxByType, GlLoadingIcon, + GlSearchBoxByType, GlTooltipDirective, } from '@gitlab/ui'; import { __ } from '~/locale'; @@ -47,7 +47,8 @@ export default { }, iid: { type: String, - required: true, + required: false, + default: null, }, value: { type: Array, @@ -167,13 +168,10 @@ export default { return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading; }, users() { - if (!this.participants) { - return []; - } - - const filteredParticipants = this.participants.filter( - (user) => user.name.includes(this.search) || user.username.includes(this.search), - ); + const filteredParticipants = + this.participants?.filter( + (user) => user.name.includes(this.search) || user.username.includes(this.search), + ) || []; // TODO this de-duplication is temporary (BE fix required) // https://gitlab.com/gitlab-org/gitlab/-/issues/327822 @@ -254,6 +252,10 @@ export default { this.$emit('input', selected); } }, + unassign() { + this.$emit('input', []); + this.$refs.dropdown.hide(); + }, unselect(name) { const selected = this.value.filter((user) => user.username !== name); this.$emit('input', selected); @@ -323,7 +325,7 @@ export default { :is-checked="selectedIsEmpty" is-check-centered data-testid="unassign" - @click.native.capture.stop="$emit('input', [])" + @click.native.capture.stop="unassign" > <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{ $options.i18n.unassigned diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js index 41aff202f48..f9b725ed429 100644 --- a/app/assets/javascripts/whats_new/utils/notification.js +++ b/app/assets/javascripts/whats_new/utils/notification.js @@ -5,6 +5,8 @@ export const getVersionDigest = (appEl) => appEl.dataset.versionDigest; export const setNotification = (appEl) => { const versionDigest = getVersionDigest(appEl); const notificationEl = document.querySelector('.header-help'); + if (!notificationEl) return; + let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count'); if (localStorage.getItem(STORAGE_KEY) === versionDigest) { diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 59a9df9ede0..575fbc03f46 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -1,6 +1,7 @@ .super-sidebar { top: 0; width: $contextual-sidebar-width; + z-index: 600; .user-bar { background-color: $t-gray-a-04; |