diff options
Diffstat (limited to 'app/assets/javascripts/alert_management/components')
12 files changed, 710 insertions, 290 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index ed6b4b7fdb2..0731349630c 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -12,18 +12,21 @@ import { GlTable, } from '@gitlab/ui'; import { s__ } from '~/locale'; -import query from '../graphql/queries/details.query.graphql'; +import alertQuery from '../graphql/queries/details.query.graphql'; +import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql'; import { fetchPolicies } from '~/lib/graphql'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import initUserPopovers from '~/user_popovers'; import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants'; -import createIssueQuery from '../graphql/mutations/create_issue_from_alert.graphql'; +import createIssueMutation from '../graphql/mutations/create_issue_from_alert.mutation.graphql'; +import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import Tracking from '~/tracking'; import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import SystemNote from './system_notes/system_note.vue'; import AlertSidebar from './alert_sidebar.vue'; +import AlertMetrics from './alert_metrics.vue'; const containerEl = document.querySelector('.page-with-contextual-sidebar'); @@ -34,6 +37,7 @@ export default { ), fullAlertDetailsTitle: s__('AlertManagement|Alert details'), overviewTitle: s__('AlertManagement|Overview'), + metricsTitle: s__('AlertManagement|Metrics'), reportedAt: s__('AlertManagement|Reported %{when}'), reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'), }, @@ -51,25 +55,29 @@ export default { TimeAgoTooltip, AlertSidebar, SystemNote, + AlertMetrics, }, - props: { + inject: { + projectPath: { + default: '', + }, alertId: { type: String, - required: true, + default: '', }, - projectPath: { + projectId: { type: String, - required: true, + default: '', }, projectIssuesPath: { type: String, - required: true, + default: '', }, }, apollo: { alert: { fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, - query, + query: alertQuery, variables() { return { fullPath: this.projectPath, @@ -84,15 +92,18 @@ export default { Sentry.captureException(error); }, }, + sidebarStatus: { + query: sidebarStatusQuery, + }, }, data() { return { alert: null, errored: false, + sidebarStatus: false, isErrorDismissed: false, createIssueError: '', issueCreationInProgress: false, - sidebarCollapsed: false, sidebarErrorMessage: '', }; }, @@ -128,10 +139,10 @@ export default { this.sidebarErrorMessage = ''; }, toggleSidebar() { - this.sidebarCollapsed = !this.sidebarCollapsed; + this.$apollo.mutate({ mutation: toggleSidebarStatusMutation }); toggleContainerClasses(containerEl, { - 'right-sidebar-collapsed': this.sidebarCollapsed, - 'right-sidebar-expanded': !this.sidebarCollapsed, + 'right-sidebar-collapsed': !this.sidebarStatus, + 'right-sidebar-expanded': this.sidebarStatus, }); }, handleAlertSidebarError(errorMessage) { @@ -143,7 +154,7 @@ export default { this.$apollo .mutate({ - mutation: createIssueQuery, + mutation: createIssueMutation, variables: { iid: this.alert.iid, projectPath: this.projectPath, @@ -169,9 +180,6 @@ export default { const { category, action } = trackAlertsDetailsViewsOptions; Tracking.event(category, action); }, - alertRefresh() { - this.$apollo.queries.alert.refetch(); - }, }, }; </script> @@ -179,7 +187,7 @@ export default { <template> <div> <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError"> - {{ sidebarErrorMessage || $options.i18n.errorMsg }} + <p v-html="sidebarErrorMessage || $options.i18n.errorMsg"></p> </gl-alert> <gl-alert v-if="createIssueError" @@ -193,10 +201,10 @@ export default { <div v-if="alert" class="alert-management-details gl-relative" - :class="{ 'pr-sm-8': sidebarCollapsed }" + :class="{ 'pr-sm-8': sidebarStatus }" > <div - class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row" + class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid flex-column flex-sm-row" > <div data-testid="alert-header" @@ -324,14 +332,14 @@ export default { </template> </gl-table> </gl-tab> + <gl-tab data-testId="metricsTab" :title="$options.i18n.metricsTitle"> + <alert-metrics :dashboard-url="alert.metricsDashboardUrl" /> + </gl-tab> </gl-tabs> <alert-sidebar - :project-path="projectPath" :alert="alert" - :sidebar-collapsed="sidebarCollapsed" - @alert-refresh="alertRefresh" @toggle-sidebar="toggleSidebar" - @alert-sidebar-error="handleAlertSidebarError" + @alert-error="handleAlertSidebarError" /> </div> </div> diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue new file mode 100644 index 00000000000..13b6a8e6653 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue @@ -0,0 +1,90 @@ +<script> +import { GlEmptyState, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + i18n: { + emptyState: { + opsgenie: { + title: s__('AlertManagement|Opsgenie is enabled'), + info: s__( + 'AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie.', + ), + buttonText: s__('AlertManagement|View alerts in Opsgenie'), + }, + gitlab: { + title: s__('AlertManagement|Surface alerts in GitLab'), + info: s__( + 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', + ), + buttonText: s__('AlertManagement|Authorize external service'), + }, + }, + moreInformation: s__('AlertManagement|More information'), + }, + components: { + GlEmptyState, + GlButton, + }, + props: { + enableAlertManagementPath: { + type: String, + required: true, + }, + userCanEnableAlertManagement: { + type: Boolean, + required: true, + }, + emptyAlertSvgPath: { + type: String, + required: true, + }, + opsgenieMvcEnabled: { + type: Boolean, + required: false, + default: false, + }, + opsgenieMvcTargetUrl: { + type: String, + required: false, + default: '', + }, + }, + computed: { + emptyState() { + return { + ...(this.opsgenieMvcEnabled + ? this.$options.i18n.emptyState.opsgenie + : this.$options.i18n.emptyState.gitlab), + link: this.opsgenieMvcEnabled ? this.opsgenieMvcTargetUrl : this.enableAlertManagementPath, + }; + }, + alertsCanBeEnabled() { + return this.userCanEnableAlertManagement || this.opsgenieMvcEnabled; + }, + }, +}; +</script> +<template> + <div> + <gl-empty-state :title="emptyState.title" :svg-path="emptyAlertSvgPath"> + <template #description> + <div class="gl-display-block"> + <span>{{ emptyState.info }}</span> + <a + v-if="!opsgenieMvcEnabled" + href="/help/user/project/operations/alert_management.html" + target="_blank" + > + {{ $options.i18n.moreInformation }} + </a> + </div> + <div v-if="alertsCanBeEnabled" class="gl-display-block center gl-pt-4"> + <gl-button category="primary" variant="success" :href="emptyState.link"> + {{ emptyState.buttonText }} + </gl-button> + </div> + </template> + </gl-empty-state> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue new file mode 100644 index 00000000000..094f33fed3b --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue @@ -0,0 +1,75 @@ +<script> +import Tracking from '~/tracking'; +import { trackAlertListViewsOptions } from '../constants'; +import AlertManagementEmptyState from './alert_management_empty_state.vue'; +import AlertManagementTable from './alert_management_table.vue'; + +export default { + components: { + AlertManagementEmptyState, + AlertManagementTable, + }, + props: { + projectPath: { + type: String, + required: true, + }, + alertManagementEnabled: { + type: Boolean, + required: true, + }, + enableAlertManagementPath: { + type: String, + required: true, + }, + populatingAlertsHelpUrl: { + type: String, + required: true, + }, + userCanEnableAlertManagement: { + type: Boolean, + required: true, + }, + emptyAlertSvgPath: { + type: String, + required: true, + }, + opsgenieMvcEnabled: { + type: Boolean, + required: false, + default: false, + }, + opsgenieMvcTargetUrl: { + type: String, + required: false, + default: '', + }, + }, + mounted() { + this.trackPageViews(); + }, + methods: { + trackPageViews() { + const { category, action } = trackAlertListViewsOptions; + Tracking.event(category, action); + }, + }, +}; +</script> +<template> + <div> + <alert-management-table + v-if="alertManagementEnabled" + :populating-alerts-help-url="populatingAlertsHelpUrl" + :project-path="projectPath" + /> + <alert-management-empty-state + v-else + :empty-alert-svg-path="emptyAlertSvgPath" + :enable-alert-management-path="enableAlertManagementPath" + :user-can-enable-alert-management="userCanEnableAlertManagement" + :opsgenie-mvc-enabled="opsgenieMvcEnabled" + :opsgenie-mvc-target-url="opsgenieMvcTargetUrl" + /> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue index 37901c21f9b..7dd3d7b5dc3 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -1,23 +1,24 @@ <script> import { - GlEmptyState, - GlDeprecatedButton, GlLoadingIcon, GlTable, GlAlert, GlIcon, - GlDropdown, - GlDropdownItem, + GlLink, GlTabs, GlTab, GlBadge, GlPagination, + GlSearchBoxByType, + GlSprintf, } from '@gitlab/ui'; -import createFlash from '~/flash'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; +import { debounce, trim } from 'lodash'; import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; import { fetchPolicies } from '~/lib/graphql'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { convertToSnakeCase } from '~/lib/utils/text_utility'; +import Tracking from '~/tracking'; import getAlerts from '../graphql/queries/get_alerts.query.graphql'; import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; import { @@ -27,11 +28,10 @@ import { trackAlertListViewsOptions, trackAlertStatusUpdateOptions, } from '../constants'; -import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; -import { convertToSnakeCase } from '~/lib/utils/text_utility'; -import Tracking from '~/tracking'; +import AlertStatus from './alert_status.vue'; -const tdClass = 'table-col gl-display-flex d-md-table-cell gl-align-items-center'; +const tdClass = + 'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap'; const thClass = 'gl-hover-bg-blue-50'; const bodyTrClass = 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200'; @@ -44,54 +44,57 @@ const initialPaginationState = { lastPageSize: null, }; +const TWELVE_HOURS_IN_MS = 12 * 60 * 60 * 1000; + export default { i18n: { noAlertsMsg: s__( - "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page.", + 'AlertManagement|No alerts available to display. See %{linkStart}enabling alert management%{linkEnd} for more information on adding alerts to the list.', ), errorMsg: s__( "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.", ), + searchPlaceholder: __('Search or filter results...'), }, fields: [ { key: 'severity', label: s__('AlertManagement|Severity'), tdClass: `${tdClass} rounded-top text-capitalize`, - thClass, + thClass: `${thClass} gl-w-eighth`, sortable: true, }, { key: 'startedAt', label: s__('AlertManagement|Start time'), - thClass: `${thClass} js-started-at`, - tdClass, - sortable: true, - }, - { - key: 'endedAt', - label: s__('AlertManagement|End time'), - thClass, + thClass: `${thClass} js-started-at w-15p`, tdClass, sortable: true, }, { key: 'title', label: s__('AlertManagement|Alert'), - thClass: `${thClass} w-30p gl-pointer-events-none`, + thClass: `gl-pointer-events-none`, tdClass, - sortable: false, }, { key: 'eventCount', label: s__('AlertManagement|Events'), - thClass: `${thClass} text-right gl-pr-9 w-3rem`, + thClass: `${thClass} text-right gl-w-12`, tdClass: `${tdClass} text-md-right`, sortable: true, }, { + key: 'issue', + label: s__('AlertManagement|Issue'), + thClass: 'gl-w-12 gl-pointer-events-none', + tdClass, + sortable: false, + }, + { key: 'assignees', label: s__('AlertManagement|Assignees'), + thClass: 'gl-w-eighth gl-pointer-events-none', tdClass, }, { @@ -102,46 +105,29 @@ export default { sortable: true, }, ], - statuses: { - TRIGGERED: s__('AlertManagement|Triggered'), - ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), - RESOLVED: s__('AlertManagement|Resolved'), - }, severityLabels: ALERTS_SEVERITY_LABELS, statusTabs: ALERTS_STATUS_TABS, components: { - GlEmptyState, GlLoadingIcon, GlTable, GlAlert, - GlDeprecatedButton, TimeAgo, - GlDropdown, - GlDropdownItem, GlIcon, + GlLink, GlTabs, GlTab, GlBadge, GlPagination, + GlSearchBoxByType, + GlSprintf, + AlertStatus, }, props: { projectPath: { type: String, required: true, }, - alertManagementEnabled: { - type: Boolean, - required: true, - }, - enableAlertManagementPath: { - type: String, - required: true, - }, - userCanEnableAlertManagement: { - type: Boolean, - required: true, - }, - emptyAlertSvgPath: { + populatingAlertsHelpUrl: { type: String, required: true, }, @@ -152,6 +138,7 @@ export default { query: getAlerts, variables() { return { + searchTerm: this.searchTerm, projectPath: this.projectPath, statuses: this.statusFilter, sort: this.sort, @@ -164,9 +151,20 @@ export default { update(data) { const { alertManagementAlerts: { nodes: list = [], pageInfo = {} } = {} } = data.project || {}; + const now = new Date(); + + const listWithData = list.map(alert => { + const then = new Date(alert.startedAt); + const diff = now - then; + + return { + ...alert, + isNew: diff < TWELVE_HOURS_IN_MS, + }; + }); return { - list, + list: listWithData, pageInfo, }; }, @@ -178,6 +176,7 @@ export default { query: getAlertsCountByStatus, variables() { return { + searchTerm: this.searchTerm, projectPath: this.projectPath, }; }, @@ -188,7 +187,9 @@ export default { }, data() { return { + searchTerm: '', errored: false, + errorMessage: '', isAlertDismissed: false, isErrorAlertDismissed: false, sort: 'STARTED_AT_DESC', @@ -203,7 +204,11 @@ export default { computed: { showNoAlertsMsg() { return ( - !this.errored && !this.loading && this.alertsCount?.all === 0 && !this.isAlertDismissed + !this.errored && + !this.loading && + this.alertsCount?.all === 0 && + !this.searchTerm && + !this.isAlertDismissed ); }, showErrorMsg() { @@ -215,9 +220,6 @@ export default { hasAlerts() { return this.alerts?.list?.length; }, - tbodyTrClass() { - return !this.loading && this.hasAlerts ? bodyTrClass : ''; - }, showPaginationControls() { return Boolean(this.prevPage || this.nextPage); }, @@ -249,30 +251,13 @@ export default { this.resetPagination(); this.sort = `${sortingColumn}_${sortingDirection}`; }, - updateAlertStatus(status, iid) { - this.$apollo - .mutate({ - mutation: updateAlertStatus, - variables: { - iid, - status: status.toUpperCase(), - projectPath: this.projectPath, - }, - }) - .then(() => { - this.trackStatusUpdate(status); - this.$apollo.queries.alerts.refetch(); - this.$apollo.queries.alertsCount.refetch(); - this.resetPagination(); - }) - .catch(() => { - createFlash( - s__( - 'AlertManagement|There was an error while updating the status of the alert. Please try again.', - ), - ); - }); - }, + onInputChange: debounce(function debounceSearch(input) { + const trimmedInput = trim(input); + if (trimmedInput !== this.searchTerm) { + this.resetPagination(); + this.searchTerm = trimmedInput; + } + }, 500), navigateToAlertDetails({ iid }) { return visitUrl(joinPaths(window.location.pathname, iid, 'details')); }, @@ -290,6 +275,9 @@ export default { ? assignees.nodes[0]?.username : s__('AlertManagement|Unassigned'); }, + getIssueLink(item) { + return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid); + }, handlePageChange(page) { const { startCursor, endCursor } = this.alerts.pageInfo; @@ -312,20 +300,49 @@ export default { resetPagination() { this.pagination = initialPaginationState; }, + tbodyTrClass(item) { + return { + [bodyTrClass]: !this.loading && this.hasAlerts, + 'new-alert': item?.isNew, + }; + }, + handleAlertError(errorMessage) { + this.errored = true; + this.errorMessage = errorMessage; + }, + dismissError() { + this.isErrorAlertDismissed = true; + this.errorMessage = ''; + }, }, }; </script> <template> <div> - <div v-if="alertManagementEnabled" class="alert-management-list"> + <div class="alert-management-list"> <gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true"> - {{ $options.i18n.noAlertsMsg }} + <gl-sprintf :message="$options.i18n.noAlertsMsg"> + <template #link="{ content }"> + <gl-link + class="gl-display-inline-block" + :href="populatingAlertsHelpUrl" + target="_blank" + > + {{ content }} + </gl-link> + </template> + </gl-sprintf> </gl-alert> - <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true"> - {{ $options.i18n.errorMsg }} + <gl-alert + v-if="showErrorMsg" + variant="danger" + data-testid="alert-error" + @dismiss="dismissError" + > + <p v-html="errorMessage || $options.i18n.errorMsg"></p> </gl-alert> - <gl-tabs @input="filterAlertsByStatus"> + <gl-tabs content-class="gl-p-0" @input="filterAlertsByStatus"> <gl-tab v-for="tab in $options.statusTabs" :key="tab.status"> <template slot="title"> <span>{{ tab.title }}</span> @@ -336,11 +353,19 @@ export default { </gl-tab> </gl-tabs> + <div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100"> + <gl-search-box-by-type + class="gl-bg-white" + :placeholder="$options.i18n.searchPlaceholder" + @input="onInputChange" + /> + </div> + <h4 class="d-block d-md-none my-3"> {{ s__('AlertManagement|Alerts') }} </h4> <gl-table - class="alert-management-table mt-3" + class="alert-management-table" :items="alerts ? alerts.list : []" :fields="$options.fields" :show-empty="true" @@ -352,6 +377,7 @@ export default { :sort-desc.sync="sortDesc" :sort-by.sync="sortBy" sort-icon-left + fixed @row-clicked="navigateToAlertDetails" @sort-changed="fetchSortedData" > @@ -374,16 +400,19 @@ export default { <time-ago v-if="item.startedAt" :time="item.startedAt" /> </template> - <template #cell(endedAt)="{ item }"> - <time-ago v-if="item.endedAt" :time="item.endedAt" /> - </template> - <template #cell(eventCount)="{ item }"> {{ item.eventCount }} </template> <template #cell(title)="{ item }"> - <div class="gl-max-w-full text-truncate">{{ item.title }}</div> + <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> + </template> + + <template #cell(issue)="{ item }"> + <gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)"> + #{{ item.issueIid }} + </gl-link> + <div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div> </template> <template #cell(assignees)="{ item }"> @@ -393,22 +422,12 @@ export default { </template> <template #cell(status)="{ item }"> - <gl-dropdown :text="$options.statuses[item.status]" class="w-100" right> - <gl-dropdown-item - v-for="(label, field) in $options.statuses" - :key="field" - @click="updateAlertStatus(label, item.iid)" - > - <span class="d-flex"> - <gl-icon - class="flex-shrink-0 append-right-4" - :class="{ invisible: label.toUpperCase() !== item.status }" - name="mobile-issue-close" - /> - {{ label }} - </span> - </gl-dropdown-item> - </gl-dropdown> + <alert-status + :alert="item" + :project-path="projectPath" + :is-sidebar="false" + @alert-error="handleAlertError" + /> </template> <template #empty> @@ -426,36 +445,9 @@ export default { :prev-page="prevPage" :next-page="nextPage" align="center" - class="gl-pagination prepend-top-default" + class="gl-pagination gl-mt-3" @input="handlePageChange" /> </div> - <gl-empty-state - v-else - :title="s__('AlertManagement|Surface alerts in GitLab')" - :svg-path="emptyAlertSvgPath" - > - <template #description> - <div class="d-block"> - <span>{{ - s__( - 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', - ) - }}</span> - <a href="/help/user/project/operations/alert_management.html" target="_blank"> - {{ s__('AlertManagement|More information') }} - </a> - </div> - <div v-if="userCanEnableAlertManagement" class="d-block center pt-4"> - <gl-deprecated-button - category="primary" - variant="success" - :href="enableAlertManagementPath" - > - {{ s__('AlertManagement|Authorize external service') }} - </gl-deprecated-button> - </div> - </template> - </gl-empty-state> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/alert_metrics.vue b/app/assets/javascripts/alert_management/components/alert_metrics.vue new file mode 100644 index 00000000000..c5b40edc672 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_metrics.vue @@ -0,0 +1,56 @@ +<script> +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as Sentry from '@sentry/browser'; + +Vue.use(Vuex); + +export default { + props: { + dashboardUrl: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + metricEmbedComponent: null, + namespace: 'alertMetrics', + }; + }, + mounted() { + if (this.dashboardUrl) { + Promise.all([ + import('~/monitoring/components/embeds/metric_embed.vue'), + import('~/monitoring/stores'), + ]) + .then(([{ default: MetricEmbed }, { monitoringDashboard }]) => { + this.$store = new Vuex.Store({ + modules: { + [this.namespace]: monitoringDashboard, + }, + }); + this.metricEmbedComponent = MetricEmbed; + }) + .catch(e => Sentry.captureException(e)); + } + }, +}; +</script> + +<template> + <div class="gl-py-3"> + <div v-if="dashboardUrl" ref="metricsChart"> + <component + :is="metricEmbedComponent" + v-if="metricEmbedComponent" + :dashboard-url="dashboardUrl" + :namespace="namespace" + /> + </div> + <div v-else ref="emptyState"> + {{ s__("AlertManagement|Metrics weren't available in the alerts payload.") }} + </div> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue index dcd22e2062e..64e4089c85a 100644 --- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue +++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue @@ -4,6 +4,8 @@ import SidebarTodo from './sidebar/sidebar_todo.vue'; import SidebarStatus from './sidebar/sidebar_status.vue'; import SidebarAssignees from './sidebar/sidebar_assignees.vue'; +import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql'; + export default { components: { SidebarAssignees, @@ -11,23 +13,34 @@ export default { SidebarTodo, SidebarStatus, }, - props: { - sidebarCollapsed: { - type: Boolean, - required: true, - }, + inject: { projectPath: { + default: '', + }, + projectId: { type: String, - required: true, + default: '', }, + }, + props: { alert: { type: Object, required: true, }, }, + apollo: { + sidebarStatus: { + query: sidebarStatusQuery, + }, + }, + data() { + return { + sidebarStatus: false, + }; + }, computed: { sidebarCollapsedClass() { - return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded'; + return this.sidebarStatus ? 'right-sidebar-collapsed' : 'right-sidebar-expanded'; }, }, }; @@ -37,23 +50,32 @@ export default { <aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar"> <div class="issuable-sidebar js-issuable-update"> <sidebar-header - :sidebar-collapsed="sidebarCollapsed" + :sidebar-collapsed="sidebarStatus" + :project-path="projectPath" + :alert="alert" @toggle-sidebar="$emit('toggle-sidebar')" + @alert-error="$emit('alert-error', $event)" + /> + <sidebar-todo + v-if="sidebarStatus" + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarStatus" + @alert-error="$emit('alert-error', $event)" /> - <sidebar-todo v-if="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" /> <sidebar-status :project-path="projectPath" :alert="alert" @toggle-sidebar="$emit('toggle-sidebar')" - @alert-sidebar-error="$emit('alert-sidebar-error', $event)" + @alert-error="$emit('alert-error', $event)" /> <sidebar-assignees :project-path="projectPath" + :project-id="projectId" :alert="alert" - :sidebar-collapsed="sidebarCollapsed" - @alert-refresh="$emit('alert-refresh')" + :sidebar-collapsed="sidebarStatus" @toggle-sidebar="$emit('toggle-sidebar')" - @alert-sidebar-error="$emit('alert-sidebar-error', $event)" + @alert-error="$emit('alert-error', $event)" /> <div class="block"></div> </div> diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue new file mode 100644 index 00000000000..9b726fe2944 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_status.vue @@ -0,0 +1,129 @@ +<script> +import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import Tracking from '~/tracking'; +import { trackAlertStatusUpdateOptions } from '../constants'; +import updateAlertStatus from '../graphql/mutations/update_alert_status.mutation.graphql'; + +export default { + i18n: { + UPDATE_ALERT_STATUS_ERROR: s__( + 'AlertManagement|There was an error while updating the status of the alert.', + ), + UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'), + }, + statuses: { + TRIGGERED: s__('AlertManagement|Triggered'), + ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), + RESOLVED: s__('AlertManagement|Resolved'), + }, + components: { + GlDropdown, + GlDropdownItem, + GlButton, + }, + props: { + projectPath: { + type: String, + required: true, + }, + alert: { + type: Object, + required: true, + }, + isDropdownShowing: { + type: Boolean, + required: false, + }, + isSidebar: { + type: Boolean, + required: true, + }, + }, + computed: { + dropdownClass() { + // eslint-disable-next-line no-nested-ternary + return this.isSidebar ? (this.isDropdownShowing ? 'show' : 'gl-display-none') : ''; + }, + }, + methods: { + updateAlertStatus(status) { + this.$emit('handle-updating', true); + this.$apollo + .mutate({ + mutation: updateAlertStatus, + variables: { + iid: this.alert.iid, + status: status.toUpperCase(), + projectPath: this.projectPath, + }, + }) + .then(resp => { + this.trackStatusUpdate(status); + this.$emit('hide-dropdown'); + + const errors = resp.data?.updateAlertStatus?.errors || []; + + if (errors[0]) { + this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_STATUS_ERROR} ${errors[0]}`, + ); + } + }) + .catch(() => { + this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_STATUS_ERROR} ${this.$options.i18n.UPDATE_ALERT_STATUS_INSTRUCTION}`, + ); + }) + .finally(() => { + this.$emit('handle-updating', false); + }); + }, + trackStatusUpdate(status) { + const { category, action, label } = trackAlertStatusUpdateOptions; + Tracking.event(category, action, { label, property: status }); + }, + }, +}; +</script> + +<template> + <div class="dropdown dropdown-menu-selectable" :class="dropdownClass"> + <gl-dropdown + ref="dropdown" + right + :text="$options.statuses[alert.status]" + class="w-100" + toggle-class="dropdown-menu-toggle" + variant="outline-default" + @keydown.esc.native="$emit('hide-dropdown')" + @hide="$emit('hide-dropdown')" + > + <div v-if="isSidebar" class="dropdown-title text-center"> + <span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + class="dropdown-title-button dropdown-menu-close" + icon="close" + @click="$emit('hide-dropdown')" + /> + </div> + <div class="dropdown-content dropdown-body"> + <gl-dropdown-item + v-for="(label, field) in $options.statuses" + :key="field" + data-testid="statusDropdownItem" + class="gl-vertical-align-middle" + :active="label.toUpperCase() === alert.status" + :active-class="'is-active'" + @click="updateAlertStatus(label)" + > + {{ label }} + </gl-dropdown-item> + </div> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue index 453a3901665..cb32a5ffd4f 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -11,20 +11,26 @@ import { GlSprintf, } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; -import { s__ } from '~/locale'; -import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.graphql'; +import { s__, __ } from '~/locale'; +import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql'; import SidebarAssignee from './sidebar_assignee.vue'; import { debounce } from 'lodash'; const DATA_REFETCH_DELAY = 250; export default { - FETCH_USERS_ERROR: s__( - 'AlertManagement|There was an error while updating the assignee(s) list. Please try again.', - ), - UPDATE_ALERT_ASSIGNEES_ERROR: s__( - 'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.', - ), + i18n: { + FETCH_USERS_ERROR: s__( + 'AlertManagement|There was an error while updating the assignee(s) list. Please try again.', + ), + UPDATE_ALERT_ASSIGNEES_ERROR: s__( + 'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.', + ), + UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR: s__( + 'AlertManagement|This assignee cannot be assigned to this alert.', + ), + ASSIGNEES_BLOCK: s__('AlertManagement|Alert assignee(s): %{assignees}'), + }, components: { GlIcon, GlDropdown, @@ -38,6 +44,10 @@ export default { SidebarAssignee, }, props: { + projectId: { + type: String, + required: true, + }, projectPath: { type: String, required: true, @@ -73,7 +83,7 @@ export default { return this.alert?.assignees?.nodes[0]?.username; }, assignedUser() { - return this.userName || s__('AlertManagement|None'); + return this.userName || __('None'); }, sortedUsers() { return this.users @@ -122,20 +132,20 @@ export default { updateAssigneesDropdown() { this.isDropdownSearching = true; return axios - .get(this.buildUrl(gon.relative_url_root, '/autocomplete/users.json'), { + .get(this.buildUrl(gon.relative_url_root, '/-/autocomplete/users.json'), { params: { search: this.search, per_page: 20, active: true, current_user: true, - project_id: gon?.current_project_id, + project_id: this.projectId, }, }) .then(({ data }) => { this.users = data; }) .catch(() => { - this.$emit('alert-sidebar-error', this.$options.FETCH_USERS_ERROR); + this.$emit('alert-error', this.$options.i18n.FETCH_USERS_ERROR); }) .finally(() => { this.isDropdownSearching = false; @@ -152,12 +162,18 @@ export default { projectPath: this.projectPath, }, }) - .then(() => { + .then(({ data: { alertSetAssignees: { errors } = [] } = {} } = {}) => { this.hideDropdown(); - this.$emit('alert-refresh'); + + if (errors[0]) { + this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR} ${errors[0]}.`, + ); + } }) .catch(() => { - this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR); + this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_ASSIGNEES_ERROR); }) .finally(() => { this.isUpdating = false; @@ -174,7 +190,7 @@ export default { <gl-loading-icon v-if="isUpdating" /> </div> <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left"> - <gl-sprintf :message="s__('AlertManagement|Alert assignee(s): %{assignees}')"> + <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK"> <template #assignees> {{ assignedUser }} </template> @@ -183,7 +199,7 @@ export default { <div class="hide-collapsed"> <p class="title gl-display-flex gl-justify-content-space-between"> - {{ s__('AlertManagement|Assignee') }} + {{ __('Assignee') }} <a v-if="isEditable" ref="editButton" @@ -192,7 +208,7 @@ export default { @click="toggleFormDropdown" @keydown.esc="hideDropdown" > - {{ s__('AlertManagement|Edit') }} + {{ __('Edit') }} </a> </p> @@ -207,7 +223,7 @@ export default { @hide="hideDropdown" > <div class="dropdown-title"> - <span class="alert-title">{{ s__('AlertManagement|Assign To') }}</span> + <span class="alert-title">{{ __('Assign To') }}</span> <gl-button :aria-label="__('Close')" variant="link" @@ -232,12 +248,12 @@ export default { active-class="is-active" @click="updateAlertAssignees('')" > - {{ s__('AlertManagement|Unassigned') }} + {{ __('Unassigned') }} </gl-dropdown-item> <gl-dropdown-divider /> <gl-dropdown-header class="mt-0"> - {{ s__('AlertManagement|Assignee') }} + {{ __('Assignee') }} </gl-dropdown-header> <sidebar-assignee v-for="user in sortedUsers" @@ -248,7 +264,7 @@ export default { /> </template> <gl-dropdown-item v-else-if="userListEmpty"> - {{ s__('AlertManagement|No Matching Results') }} + {{ __('No Matching Results') }} </gl-dropdown-item> <gl-loading-icon v-else /> </div> @@ -261,7 +277,7 @@ export default { assignedUser }}</span> <span v-else class="gl-display-flex gl-align-items-center"> - {{ s__('AlertManagement|None -') }} + {{ __('None') }} - <gl-button class="gl-pl-2" href="#" @@ -269,7 +285,7 @@ export default { data-testid="unassigned-users" @click="updateAlertAssignees(currentUser)" > - {{ s__('AlertManagement| assign yourself') }} + {{ __('assign yourself') }} </gl-button> </span> </p> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue index 047793d8cee..fd40b5d9f65 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue @@ -8,6 +8,14 @@ export default { SidebarTodo, }, props: { + alert: { + type: Object, + required: true, + }, + projectPath: { + type: String, + required: true, + }, sidebarCollapsed: { type: Boolean, required: true, @@ -17,18 +25,17 @@ export default { </script> <template> - <div class="block d-flex justify-content-between"> + <div class="block gl-display-flex gl-justify-content-space-between"> <span class="issuable-header-text hide-collapsed"> - {{ __('Quick actions') }} + {{ __('To Do') }} </span> - <toggle-sidebar - :collapsed="sidebarCollapsed" - css-classes="ml-auto" - @toggle="$emit('toggle-sidebar')" + <sidebar-todo + v-if="!sidebarCollapsed" + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarCollapsed" + @alert-error="$emit('alert-error', $event)" /> - <!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 --> - <template v-if="false"> - <sidebar-todo v-if="!sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed" /> - </template> + <toggle-sidebar :collapsed="sidebarCollapsed" @toggle="$emit('toggle-sidebar')" /> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue index 89dbbedd9c1..44a81aba828 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue @@ -1,17 +1,7 @@ <script> -import { - GlIcon, - GlDropdown, - GlDropdownItem, - GlLoadingIcon, - GlTooltip, - GlButton, - GlSprintf, -} from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; -import Tracking from '~/tracking'; -import { trackAlertStatusUpdateOptions } from '../../constants'; -import updateAlertStatus from '../../graphql/mutations/update_alert_status.graphql'; +import AlertStatus from '../alert_status.vue'; export default { statuses: { @@ -21,12 +11,10 @@ export default { }, components: { GlIcon, - GlDropdown, - GlDropdownItem, GlLoadingIcon, GlTooltip, - GlButton, GlSprintf, + AlertStatus, }, props: { projectPath: { @@ -60,44 +48,13 @@ export default { }, toggleFormDropdown() { this.isDropdownShowing = !this.isDropdownShowing; - const { dropdown } = this.$refs.dropdown.$refs; + const { dropdown } = this.$children[2].$refs.dropdown.$refs; if (dropdown && this.isDropdownShowing) { dropdown.show(); } }, - isSelected(status) { - return this.alert.status === status; - }, - updateAlertStatus(status) { - this.isUpdating = true; - this.$apollo - .mutate({ - mutation: updateAlertStatus, - variables: { - iid: this.alert.iid, - status: status.toUpperCase(), - projectPath: this.projectPath, - }, - }) - .then(() => { - this.trackStatusUpdate(status); - this.hideDropdown(); - }) - .catch(() => { - this.$emit( - 'alert-sidebar-error', - s__( - 'AlertManagement|There was an error while updating the status of the alert. Please try again.', - ), - ); - }) - .finally(() => { - this.isUpdating = false; - }); - }, - trackStatusUpdate(status) { - const { category, action, label } = trackAlertStatusUpdateOptions; - Tracking.event(category, action, { label, property: status }); + handleUpdating(updating) { + this.isUpdating = updating; }, }, }; @@ -132,41 +89,15 @@ export default { </a> </p> - <div class="dropdown dropdown-menu-selectable" :class="dropdownClass"> - <gl-dropdown - ref="dropdown" - :text="$options.statuses[alert.status]" - class="w-100" - toggle-class="dropdown-menu-toggle" - variant="outline-default" - @keydown.esc.native="hideDropdown" - @hide="hideDropdown" - > - <div class="dropdown-title"> - <span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span> - <gl-button - :aria-label="__('Close')" - variant="link" - class="dropdown-title-button dropdown-menu-close" - icon="close" - @click="hideDropdown" - /> - </div> - <div class="dropdown-content dropdown-body"> - <gl-dropdown-item - v-for="(label, field) in $options.statuses" - :key="field" - data-testid="statusDropdownItem" - class="gl-vertical-align-middle" - :active="label.toUpperCase() === alert.status" - :active-class="'is-active'" - @click="updateAlertStatus(label)" - > - {{ label }} - </gl-dropdown-item> - </div> - </gl-dropdown> - </div> + <alert-status + :alert="alert" + :project-path="projectPath" + :is-dropdown-showing="isDropdownShowing" + :is-sidebar="true" + @alert-error="$emit('alert-error', $event)" + @hide-dropdown="hideDropdown" + @handle-updating="handleUpdating" + /> <gl-loading-icon v-if="isUpdating" :inline="true" /> <p diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue index 87090165f82..7d3135ad50d 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue @@ -1,29 +1,123 @@ <script> +import { s__ } from '~/locale'; import Todo from '~/sidebar/components/todo_toggle/todo.vue'; +import axios from '~/lib/utils/axios_utils'; +import createAlertTodo from '../../graphql/mutations/alert_todo_create.graphql'; export default { + i18n: { + UPDATE_ALERT_TODO_ERROR: s__( + 'AlertManagement|There was an error while updating the To Do of the alert.', + ), + }, components: { Todo, }, props: { + alert: { + type: Object, + required: true, + }, + projectPath: { + type: String, + required: true, + }, sidebarCollapsed: { type: Boolean, required: true, }, }, + data() { + return { + isUpdating: false, + isTodo: false, + todo: '', + }; + }, + computed: { + alertID() { + return parseInt(this.alert.iid, 10); + }, + }, + methods: { + updateToDoCount(add) { + const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10); + const count = add ? oldCount + 1 : oldCount - 1; + const headerTodoEvent = new CustomEvent('todo:toggle', { + detail: { + count, + }, + }); + + return document.dispatchEvent(headerTodoEvent); + }, + toggleTodo() { + if (this.todo) { + return this.markAsDone(); + } + + this.isUpdating = true; + return this.$apollo + .mutate({ + mutation: createAlertTodo, + variables: { + iid: this.alert.iid, + projectPath: this.projectPath, + }, + }) + .then(({ data: { alertTodoCreate: { todo = {}, errors = [] } } = {} } = {}) => { + if (errors[0]) { + return this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${errors[0]}.`, + ); + } + + this.todo = todo.id; + return this.updateToDoCount(true); + }) + .catch(() => { + this.$emit( + 'alert-error', + `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${s__( + 'AlertManagement|Please try again.', + )}`, + ); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + markAsDone() { + this.isUpdating = true; + + return axios + .delete(`/dashboard/todos/${this.todo.split('/').pop()}`) + .then(() => { + this.todo = ''; + return this.updateToDoCount(false); + }) + .catch(() => { + this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_TODO_ERROR); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + }, }; </script> -<!-- TODO: Implement after or as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/215946 --> <template> - <div v-if="false" :class="{ 'block todo': sidebarCollapsed }"> + <div :class="{ 'block todo': sidebarCollapsed, 'gl-ml-auto': !sidebarCollapsed }"> <todo + data-testid="alert-todo-button" :collapsed="sidebarCollapsed" - :issuable-id="1" - :is-todo="false" - :is-action-active="false" + :issuable-id="alertID" + :is-todo="todo !== ''" + :is-action-active="isUpdating" issuable-type="alert" - @toggleTodo="() => {}" + @toggleTodo="toggleTodo" /> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue index 9042d51aecf..39717ab609f 100644 --- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue +++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue @@ -24,7 +24,7 @@ export default { return { ...author, id: id?.split('/').pop() }; }, iconHtml() { - return spriteIcon('user'); + return spriteIcon(this.note?.systemNoteIconName); }, }, }; |