diff options
Diffstat (limited to 'app/assets/javascripts/alert_management')
22 files changed, 1195 insertions, 149 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index 89db7db77d5..ed6b4b7fdb2 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -2,31 +2,32 @@ import * as Sentry from '@sentry/browser'; import { GlAlert, + GlBadge, GlIcon, GlLoadingIcon, - GlDropdown, - GlDropdownItem, GlSprintf, GlTabs, GlTab, GlButton, GlTable, } from '@gitlab/ui'; -import createFlash from '~/flash'; import { s__ } from '~/locale'; import query from '../graphql/queries/details.query.graphql'; import { fetchPolicies } from '~/lib/graphql'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { ALERTS_SEVERITY_LABELS } from '../constants'; -import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; +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 { 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'; + +const containerEl = document.querySelector('.page-with-contextual-sidebar'); export default { - statuses: { - TRIGGERED: s__('AlertManagement|Triggered'), - ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), - RESOLVED: s__('AlertManagement|Resolved'), - }, i18n: { errorMsg: s__( 'AlertManagement|There was an error displaying the alert. Please refresh the page to try again.', @@ -38,19 +39,19 @@ export default { }, severityLabels: ALERTS_SEVERITY_LABELS, components: { + GlBadge, GlAlert, GlIcon, GlLoadingIcon, GlSprintf, - GlDropdown, - GlDropdownItem, GlTab, GlTabs, GlButton, GlTable, TimeAgoTooltip, + AlertSidebar, + SystemNote, }, - mixins: [glFeatureFlagsMixin()], props: { alertId: { type: String, @@ -60,7 +61,7 @@ export default { type: String, required: true, }, - newIssuePath: { + projectIssuesPath: { type: String, required: true, }, @@ -85,7 +86,15 @@ export default { }, }, data() { - return { alert: null, errored: false, isErrorDismissed: false }; + return { + alert: null, + errored: false, + isErrorDismissed: false, + createIssueError: '', + issueCreationInProgress: false, + sidebarCollapsed: false, + sidebarErrorMessage: '', + }; }, computed: { loading() { @@ -100,38 +109,92 @@ export default { return this.errored && !this.isErrorDismissed; }, }, + mounted() { + this.trackPageViews(); + toggleContainerClasses(containerEl, { + 'issuable-bulk-update-sidebar': true, + 'right-sidebar-expanded': true, + }); + }, + updated() { + this.$nextTick(() => { + highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')); + initUserPopovers(this.$el.querySelectorAll('.js-user-link')); + }); + }, methods: { dismissError() { this.isErrorDismissed = true; + this.sidebarErrorMessage = ''; }, - updateAlertStatus(status) { + toggleSidebar() { + this.sidebarCollapsed = !this.sidebarCollapsed; + toggleContainerClasses(containerEl, { + 'right-sidebar-collapsed': this.sidebarCollapsed, + 'right-sidebar-expanded': !this.sidebarCollapsed, + }); + }, + handleAlertSidebarError(errorMessage) { + this.errored = true; + this.sidebarErrorMessage = errorMessage; + }, + createIssue() { + this.issueCreationInProgress = true; + this.$apollo .mutate({ - mutation: updateAlertStatus, + mutation: createIssueQuery, variables: { - iid: this.alertId, - status: status.toUpperCase(), + iid: this.alert.iid, projectPath: this.projectPath, }, }) - .catch(() => { - createFlash( - s__( - 'AlertManagement|There was an error while updating the status of the alert. Please try again.', - ), - ); + .then(({ data: { createAlertIssue: { errors, issue } } }) => { + if (errors?.length) { + [this.createIssueError] = errors; + this.issueCreationInProgress = false; + } else if (issue) { + visitUrl(this.issuePath(issue.iid)); + } + }) + .catch(error => { + this.createIssueError = error; + this.issueCreationInProgress = false; }); }, + issuePath(issueId) { + return joinPaths(this.projectIssuesPath, issueId); + }, + trackPageViews() { + const { category, action } = trackAlertsDetailsViewsOptions; + Tracking.event(category, action); + }, + alertRefresh() { + this.$apollo.queries.alert.refetch(); + }, }, }; </script> + <template> <div> <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError"> - {{ $options.i18n.errorMsg }} + {{ sidebarErrorMessage || $options.i18n.errorMsg }} + </gl-alert> + <gl-alert + v-if="createIssueError" + variant="danger" + data-testid="issueCreationError" + @dismiss="createIssueError = null" + > + {{ createIssueError }} </gl-alert> <div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div> - <div v-if="alert" class="alert-management-details gl-relative"> + <div + v-if="alert" + class="alert-management-details gl-relative" + :class="{ 'pr-sm-8': sidebarCollapsed }" + > <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" > @@ -142,32 +205,50 @@ export default { <div class="gl-display-inline-flex gl-align-items-center gl-justify-content-space-between" > - <gl-icon - class="gl-mr-3 align-middle" - :size="12" - :name="`severity-${alert.severity.toLowerCase()}`" - :class="`icon-${alert.severity.toLowerCase()}`" - /> - <strong>{{ $options.severityLabels[alert.severity] }}</strong> + <gl-badge class="gl-mr-3"> + <strong>{{ s__('AlertManagement|Alert') }}</strong> + </gl-badge> </div> - <span class="mx-2">•</span> - <gl-sprintf :message="reportedAtMessage"> - <template #when> - <time-ago-tooltip :time="alert.createdAt" class="gl-ml-3" /> - </template> - <template #tool>{{ alert.monitoringTool }}</template> - </gl-sprintf> + <span> + <gl-sprintf :message="reportedAtMessage"> + <template #when> + <time-ago-tooltip :time="alert.createdAt" /> + </template> + <template #tool>{{ alert.monitoringTool }}</template> + </gl-sprintf> + </span> </div> <gl-button - v-if="glFeatures.createIssueFromAlertEnabled" - class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-create-issue-button" + v-if="alert.issueIid" + class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button" + data-testid="viewIssueBtn" + :href="issuePath(alert.issueIid)" + category="primary" + variant="success" + > + {{ s__('AlertManagement|View issue') }} + </gl-button> + <gl-button + v-else + class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button" data-testid="createIssueBtn" - :href="newIssuePath" + :loading="issueCreationInProgress" category="primary" variant="success" + @click="createIssue()" > {{ s__('AlertManagement|Create issue') }} </gl-button> + <gl-button + :aria-label="__('Toggle sidebar')" + category="primary" + variant="default" + class="d-sm-none gl-absolute toggle-sidebar-mobile-button" + type="button" + @click="toggleSidebar" + > + <i class="fa fa-angle-double-left"></i> + </gl-button> </div> <div v-if="alert" @@ -175,44 +256,57 @@ export default { > <h2 data-testid="title">{{ alert.title }}</h2> </div> - <gl-dropdown :text="$options.statuses[alert.status]" class="gl-absolute gl-right-0" right> - <gl-dropdown-item - v-for="(label, field) in $options.statuses" - :key="field" - data-testid="statusDropdownItem" - class="gl-vertical-align-middle" - @click="updateAlertStatus(label)" - > - <span class="d-flex"> - <gl-icon - class="flex-shrink-0 append-right-4" - :class="{ invisible: label.toUpperCase() !== alert.status }" - name="mobile-issue-close" - /> - {{ label }} - </span> - </gl-dropdown-item> - </gl-dropdown> <gl-tabs v-if="alert" data-testid="alertDetailsTabs"> <gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle"> - <ul class="pl-4 mb-n1"> - <li v-if="alert.startedAt" class="my-2"> - <strong class="bold">{{ s__('AlertManagement|Start time') }}:</strong> + <div v-if="alert.severity" class="gl-mt-3 gl-mb-5 gl-display-flex"> + <div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> + {{ s__('AlertManagement|Severity') }}: + </div> + <div class="gl-pl-2" data-testid="severity"> + <span> + <gl-icon + class="gl-vertical-align-middle" + :size="12" + :name="`severity-${alert.severity.toLowerCase()}`" + :class="`icon-${alert.severity.toLowerCase()}`" + /> + </span> + {{ $options.severityLabels[alert.severity] }} + </div> + </div> + <div v-if="alert.startedAt" class="gl-my-5 gl-display-flex"> + <div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> + {{ s__('AlertManagement|Start time') }}: + </div> + <div class="gl-pl-2"> <time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" /> - </li> - <li v-if="alert.eventCount" class="my-2"> - <strong class="bold">{{ s__('AlertManagement|Events') }}:</strong> - <span data-testid="eventCount">{{ alert.eventCount }}</span> - </li> - <li v-if="alert.monitoringTool" class="my-2"> - <strong class="bold">{{ s__('AlertManagement|Tool') }}:</strong> - <span data-testid="monitoringTool">{{ alert.monitoringTool }}</span> - </li> - <li v-if="alert.service" class="my-2"> - <strong class="bold">{{ s__('AlertManagement|Service') }}:</strong> - <span data-testid="service">{{ alert.service }}</span> - </li> - </ul> + </div> + </div> + <div v-if="alert.eventCount" class="gl-my-5 gl-display-flex"> + <div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> + {{ s__('AlertManagement|Events') }}: + </div> + <div class="gl-pl-2" data-testid="eventCount">{{ alert.eventCount }}</div> + </div> + <div v-if="alert.monitoringTool" class="gl-my-5 gl-display-flex"> + <div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3"> + {{ s__('AlertManagement|Tool') }}: + </div> + <div class="gl-pl-2" data-testid="monitoringTool">{{ alert.monitoringTool }}</div> + </div> + <div v-if="alert.service" class="gl-my-5 gl-display-flex"> + <div class="bold gl-w-13 gl-text-right gl-pr-3"> + {{ s__('AlertManagement|Service') }}: + </div> + <div class="gl-pl-2" data-testid="service">{{ alert.service }}</div> + </div> + <template> + <div v-if="alert.notes.nodes" class="issuable-discussion py-5"> + <ul class="notes main-notes-list timeline"> + <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" /> + </ul> + </div> + </template> </gl-tab> <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle"> <gl-table @@ -231,6 +325,14 @@ export default { </gl-table> </gl-tab> </gl-tabs> + <alert-sidebar + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarCollapsed" + @alert-refresh="alertRefresh" + @toggle-sidebar="toggleSidebar" + @alert-sidebar-error="handleAlertSidebarError" + /> </div> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index 74fc19ff3d4..37901c21f9b 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -10,23 +10,41 @@ import { GlDropdownItem, GlTabs, GlTab, + GlBadge, + GlPagination, } from '@gitlab/ui'; import createFlash from '~/flash'; import { s__ } from '~/locale'; import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; +import { fetchPolicies } from '~/lib/graphql'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import getAlerts from '../graphql/queries/getAlerts.query.graphql'; -import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import getAlerts from '../graphql/queries/get_alerts.query.graphql'; +import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; +import { + ALERTS_STATUS_TABS, + ALERTS_SEVERITY_LABELS, + DEFAULT_PAGE_SIZE, + trackAlertListViewsOptions, + trackAlertStatusUpdateOptions, +} from '../constants'; import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { convertToSnakeCase } from '~/lib/utils/text_utility'; +import Tracking from '~/tracking'; -const tdClass = 'table-col d-flex d-md-table-cell align-items-center'; +const tdClass = 'table-col gl-display-flex d-md-table-cell gl-align-items-center'; +const thClass = 'gl-hover-bg-blue-50'; const bodyTrClass = - 'gl-border-1 gl-border-t-solid gl-border-gray-100 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200'; + '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'; + +const initialPaginationState = { + currentPage: 1, + prevPageCursor: '', + nextPageCursor: '', + firstPageSize: DEFAULT_PAGE_SIZE, + lastPageSize: null, +}; export default { - bodyTrClass, i18n: { noAlertsMsg: s__( "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page.", @@ -40,40 +58,54 @@ export default { key: 'severity', label: s__('AlertManagement|Severity'), tdClass: `${tdClass} rounded-top text-capitalize`, + thClass, + 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, tdClass, + sortable: true, }, { key: 'title', label: s__('AlertManagement|Alert'), - thClass: 'w-30p', + thClass: `${thClass} w-30p gl-pointer-events-none`, tdClass, + sortable: false, }, { key: 'eventCount', label: s__('AlertManagement|Events'), - thClass: 'text-right event-count', - tdClass: `${tdClass} text-md-right event-count`, + thClass: `${thClass} text-right gl-pr-9 w-3rem`, + tdClass: `${tdClass} text-md-right`, + sortable: true, + }, + { + key: 'assignees', + label: s__('AlertManagement|Assignees'), + tdClass, }, { key: 'status', - thClass: 'w-15p', + thClass: `${thClass} w-15p`, label: s__('AlertManagement|Status'), tdClass: `${tdClass} rounded-bottom`, + sortable: true, }, ], statuses: { - [ALERTS_STATUS.TRIGGERED]: s__('AlertManagement|Triggered'), - [ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'), - [ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'), + TRIGGERED: s__('AlertManagement|Triggered'), + ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), + RESOLVED: s__('AlertManagement|Resolved'), }, severityLabels: ALERTS_SEVERITY_LABELS, statusTabs: ALERTS_STATUS_TABS, @@ -89,8 +121,9 @@ export default { GlIcon, GlTabs, GlTab, + GlBadge, + GlPagination, }, - mixins: [glFeatureFlagsMixin()], props: { projectPath: { type: String, @@ -115,33 +148,63 @@ export default { }, apollo: { alerts: { + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, query: getAlerts, variables() { return { projectPath: this.projectPath, statuses: this.statusFilter, + sort: this.sort, + firstPageSize: this.pagination.firstPageSize, + lastPageSize: this.pagination.lastPageSize, + prevPageCursor: this.pagination.prevPageCursor, + nextPageCursor: this.pagination.nextPageCursor, }; }, update(data) { - return data.project.alertManagementAlerts.nodes; + const { alertManagementAlerts: { nodes: list = [], pageInfo = {} } = {} } = + data.project || {}; + + return { + list, + pageInfo, + }; }, error() { this.errored = true; }, }, + alertsCount: { + query: getAlertsCountByStatus, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data.project?.alertManagementAlertStatusCounts; + }, + }, }, data() { return { - alerts: null, errored: false, isAlertDismissed: false, isErrorAlertDismissed: false, - statusFilter: this.$options.statusTabs[4].filters, + sort: 'STARTED_AT_DESC', + statusFilter: [], + filteredByStatus: '', + pagination: initialPaginationState, + sortBy: 'startedAt', + sortDesc: true, + sortDirection: 'desc', }; }, computed: { showNoAlertsMsg() { - return !this.errored && !this.loading && !this.alerts?.length && !this.isAlertDismissed; + return ( + !this.errored && !this.loading && this.alertsCount?.all === 0 && !this.isAlertDismissed + ); }, showErrorMsg() { return this.errored && !this.isErrorAlertDismissed; @@ -149,12 +212,43 @@ export default { loading() { return this.$apollo.queries.alerts.loading; }, + hasAlerts() { + return this.alerts?.list?.length; + }, + tbodyTrClass() { + return !this.loading && this.hasAlerts ? bodyTrClass : ''; + }, + showPaginationControls() { + return Boolean(this.prevPage || this.nextPage); + }, + alertsForCurrentTab() { + return this.alertsCount ? this.alertsCount[this.filteredByStatus.toLowerCase()] : 0; + }, + prevPage() { + return Math.max(this.pagination.currentPage - 1, 0); + }, + nextPage() { + const nextPage = this.pagination.currentPage + 1; + return nextPage > Math.ceil(this.alertsForCurrentTab / DEFAULT_PAGE_SIZE) ? null : nextPage; + }, + }, + mounted() { + this.trackPageViews(); }, methods: { filterAlertsByStatus(tabIndex) { - this.statusFilter = this.$options.statusTabs[tabIndex].filters; + this.resetPagination(); + const { filters, status } = this.$options.statusTabs[tabIndex]; + this.statusFilter = filters; + this.filteredByStatus = status; + }, + fetchSortedData({ sortBy, sortDesc }) { + const sortingDirection = sortDesc ? 'DESC' : 'ASC'; + const sortingColumn = convertToSnakeCase(sortBy).toUpperCase(); + + this.resetPagination(); + this.sort = `${sortingColumn}_${sortingDirection}`; }, - capitalizeFirstCharacter, updateAlertStatus(status, iid) { this.$apollo .mutate({ @@ -166,7 +260,10 @@ export default { }, }) .then(() => { + this.trackStatusUpdate(status); this.$apollo.queries.alerts.refetch(); + this.$apollo.queries.alertsCount.refetch(); + this.resetPagination(); }) .catch(() => { createFlash( @@ -179,6 +276,42 @@ export default { navigateToAlertDetails({ iid }) { return visitUrl(joinPaths(window.location.pathname, iid, 'details')); }, + trackPageViews() { + const { category, action } = trackAlertListViewsOptions; + Tracking.event(category, action); + }, + trackStatusUpdate(status) { + const { category, action, label } = trackAlertStatusUpdateOptions; + Tracking.event(category, action, { label, property: status }); + }, + getAssignees(assignees) { + // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405 + return assignees.nodes?.length > 0 + ? assignees.nodes[0]?.username + : s__('AlertManagement|Unassigned'); + }, + handlePageChange(page) { + const { startCursor, endCursor } = this.alerts.pageInfo; + + if (page > this.pagination.currentPage) { + this.pagination = { + ...initialPaginationState, + nextPageCursor: endCursor, + currentPage: page, + }; + } else { + this.pagination = { + lastPageSize: DEFAULT_PAGE_SIZE, + firstPageSize: null, + prevPageCursor: startCursor, + nextPageCursor: '', + currentPage: page, + }; + } + }, + resetPagination() { + this.pagination = initialPaginationState; + }, }, }; </script> @@ -192,10 +325,13 @@ export default { {{ $options.i18n.errorMsg }} </gl-alert> - <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus"> + <gl-tabs @input="filterAlertsByStatus"> <gl-tab v-for="tab in $options.statusTabs" :key="tab.status"> <template slot="title"> <span>{{ tab.title }}</span> + <gl-badge v-if="alertsCount" pill size="sm" class="gl-tab-counter-badge"> + {{ alertsCount[tab.status.toLowerCase()] }} + </gl-badge> </template> </gl-tab> </gl-tabs> @@ -205,13 +341,19 @@ export default { </h4> <gl-table class="alert-management-table mt-3" - :items="alerts" + :items="alerts ? alerts.list : []" :fields="$options.fields" :show-empty="true" :busy="loading" stacked="md" - :tbody-tr-class="$options.bodyTrClass" + :tbody-tr-class="tbodyTrClass" + :no-local-sorting="true" + :sort-direction="sortDirection" + :sort-desc.sync="sortDesc" + :sort-by.sync="sortBy" + sort-icon-left @row-clicked="navigateToAlertDetails" + @sort-changed="fetchSortedData" > <template #cell(severity)="{ item }"> <div @@ -236,16 +378,22 @@ export default { <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> </template> + <template #cell(assignees)="{ item }"> + <div class="gl-max-w-full text-truncate" data-testid="assigneesField"> + {{ getAssignees(item.assignees) }} + </div> + </template> + <template #cell(status)="{ item }"> - <gl-dropdown - :text="capitalizeFirstCharacter(item.status.toLowerCase())" - class="w-100" - right - > + <gl-dropdown :text="$options.statuses[item.status]" class="w-100" right> <gl-dropdown-item v-for="(label, field) in $options.statuses" :key="field" @@ -271,6 +419,16 @@ export default { <gl-loading-icon size="lg" color="dark" class="mt-3" /> </template> </gl-table> + + <gl-pagination + v-if="showPaginationControls" + :value="pagination.currentPage" + :prev-page="prevPage" + :next-page="nextPage" + align="center" + class="gl-pagination prepend-top-default" + @input="handlePageChange" + /> </div> <gl-empty-state v-else diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue new file mode 100644 index 00000000000..dcd22e2062e --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue @@ -0,0 +1,61 @@ +<script> +import SidebarHeader from './sidebar/sidebar_header.vue'; +import SidebarTodo from './sidebar/sidebar_todo.vue'; +import SidebarStatus from './sidebar/sidebar_status.vue'; +import SidebarAssignees from './sidebar/sidebar_assignees.vue'; + +export default { + components: { + SidebarAssignees, + SidebarHeader, + SidebarTodo, + SidebarStatus, + }, + props: { + sidebarCollapsed: { + type: Boolean, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + alert: { + type: Object, + required: true, + }, + }, + computed: { + sidebarCollapsedClass() { + return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded'; + }, + }, +}; +</script> + +<template> + <aside :class="sidebarCollapsedClass" class="right-sidebar alert-sidebar"> + <div class="issuable-sidebar js-issuable-update"> + <sidebar-header + :sidebar-collapsed="sidebarCollapsed" + @toggle-sidebar="$emit('toggle-sidebar')" + /> + <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)" + /> + <sidebar-assignees + :project-path="projectPath" + :alert="alert" + :sidebar-collapsed="sidebarCollapsed" + @alert-refresh="$emit('alert-refresh')" + @toggle-sidebar="$emit('toggle-sidebar')" + @alert-sidebar-error="$emit('alert-sidebar-error', $event)" + /> + <div class="block"></div> + </div> + </aside> +</template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue new file mode 100644 index 00000000000..df07038151e --- /dev/null +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue @@ -0,0 +1,51 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdownItem, + }, + props: { + user: { + type: Object, + required: true, + }, + active: { + type: Boolean, + required: true, + }, + }, + methods: { + isActive(name) { + return this.alert.assignees.nodes.some(({ username }) => username === name); + }, + }, +}; +</script> + +<template> + <gl-dropdown-item + :key="user.username" + data-testid="assigneeDropdownItem" + class="assignee-dropdown-item gl-vertical-align-middle" + :active="active" + active-class="is-active" + @click="$emit('update-alert-assignees', user.username)" + > + <span class="gl-relative mr-2"> + <img + :alt="user.username" + :src="user.avatar_url" + :width="32" + class="avatar avatar-inline gl-m-0 s32" + data-qa-selector="avatar_image" + /> + </span> + <span class="d-flex gl-flex-direction-column gl-overflow-hidden"> + <strong class="dropdown-menu-user-full-name"> + {{ user.name }} + </strong> + <span class="dropdown-menu-user-username"> {{ user.username }}</span> + </span> + </gl-dropdown-item> +</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 new file mode 100644 index 00000000000..453a3901665 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -0,0 +1,278 @@ +<script> +import { + GlIcon, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + GlLoadingIcon, + GlTooltip, + GlButton, + 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 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.', + ), + components: { + GlIcon, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlDropdownHeader, + GlLoadingIcon, + GlTooltip, + GlButton, + GlSprintf, + SidebarAssignee, + }, + props: { + projectPath: { + type: String, + required: true, + }, + alert: { + type: Object, + required: true, + }, + isEditable: { + type: Boolean, + required: false, + default: true, + }, + sidebarCollapsed: { + type: Boolean, + required: false, + }, + }, + data() { + return { + isDropdownShowing: false, + isDropdownSearching: false, + isUpdating: false, + search: '', + users: [], + }; + }, + computed: { + currentUser() { + return gon?.current_username; + }, + userName() { + return this.alert?.assignees?.nodes[0]?.username; + }, + assignedUser() { + return this.userName || s__('AlertManagement|None'); + }, + sortedUsers() { + return this.users + .map(user => ({ ...user, active: this.isActive(user.username) })) + .sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1)); // eslint-disable-line no-nested-ternary + }, + dropdownClass() { + return this.isDropdownShowing ? 'show' : 'gl-display-none'; + }, + userListValid() { + return !this.isDropdownSearching && this.users.length > 0; + }, + userListEmpty() { + return !this.isDropdownSearching && this.users.length === 0; + }, + }, + watch: { + search: debounce(function debouncedUserSearch() { + this.updateAssigneesDropdown(); + }, DATA_REFETCH_DELAY), + }, + mounted() { + this.updateAssigneesDropdown(); + }, + methods: { + hideDropdown() { + this.isDropdownShowing = false; + }, + toggleFormDropdown() { + this.isDropdownShowing = !this.isDropdownShowing; + const { dropdown } = this.$refs.dropdown.$refs; + if (dropdown && this.isDropdownShowing) { + dropdown.show(); + } + }, + isActive(name) { + return this.alert.assignees.nodes.some(({ username }) => username === name); + }, + buildUrl(urlRoot, url) { + let newUrl; + if (urlRoot != null) { + newUrl = urlRoot.replace(/\/$/, '') + url; + } + return newUrl; + }, + updateAssigneesDropdown() { + this.isDropdownSearching = true; + return axios + .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, + }, + }) + .then(({ data }) => { + this.users = data; + }) + .catch(() => { + this.$emit('alert-sidebar-error', this.$options.FETCH_USERS_ERROR); + }) + .finally(() => { + this.isDropdownSearching = false; + }); + }, + updateAlertAssignees(assignees) { + this.isUpdating = true; + this.$apollo + .mutate({ + mutation: alertSetAssignees, + variables: { + iid: this.alert.iid, + assigneeUsernames: [this.isActive(assignees) ? '' : assignees], + projectPath: this.projectPath, + }, + }) + .then(() => { + this.hideDropdown(); + this.$emit('alert-refresh'); + }) + .catch(() => { + this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR); + }) + .finally(() => { + this.isUpdating = false; + }); + }, + }, +}; +</script> + +<template> + <div class="block alert-status"> + <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')"> + <gl-icon name="user" :size="14" /> + <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}')"> + <template #assignees> + {{ assignedUser }} + </template> + </gl-sprintf> + </gl-tooltip> + + <div class="hide-collapsed"> + <p class="title gl-display-flex gl-justify-content-space-between"> + {{ s__('AlertManagement|Assignee') }} + <a + v-if="isEditable" + ref="editButton" + class="btn-link" + href="#" + @click="toggleFormDropdown" + @keydown.esc="hideDropdown" + > + {{ s__('AlertManagement|Edit') }} + </a> + </p> + + <div class="dropdown dropdown-menu-selectable" :class="dropdownClass"> + <gl-dropdown + ref="dropdown" + :text="assignedUser" + 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 To') }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + class="dropdown-title-button dropdown-menu-close" + icon="close" + @click="hideDropdown" + /> + </div> + <div class="dropdown-input"> + <input + v-model.trim="search" + class="dropdown-input-field" + type="search" + :placeholder="__('Search users')" + /> + <gl-icon name="search" class="dropdown-input-search ic-search" data-hidden="true" /> + </div> + <div class="dropdown-content dropdown-body"> + <template v-if="userListValid"> + <gl-dropdown-item + :active="!userName" + active-class="is-active" + @click="updateAlertAssignees('')" + > + {{ s__('AlertManagement|Unassigned') }} + </gl-dropdown-item> + <gl-dropdown-divider /> + + <gl-dropdown-header class="mt-0"> + {{ s__('AlertManagement|Assignee') }} + </gl-dropdown-header> + <sidebar-assignee + v-for="user in sortedUsers" + :key="user.username" + :user="user" + :active="user.active" + @update-alert-assignees="updateAlertAssignees" + /> + </template> + <gl-dropdown-item v-else-if="userListEmpty"> + {{ s__('AlertManagement|No Matching Results') }} + </gl-dropdown-item> + <gl-loading-icon v-else /> + </div> + </gl-dropdown> + </div> + + <gl-loading-icon v-if="isUpdating" :inline="true" /> + <p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }"> + <span v-if="userName" class="gl-text-gray-700" data-testid="assigned-users">{{ + assignedUser + }}</span> + <span v-else class="gl-display-flex gl-align-items-center"> + {{ s__('AlertManagement|None -') }} + <gl-button + class="gl-pl-2" + href="#" + variant="link" + data-testid="unassigned-users" + @click="updateAlertAssignees(currentUser)" + > + {{ s__('AlertManagement| assign yourself') }} + </gl-button> + </span> + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue new file mode 100644 index 00000000000..047793d8cee --- /dev/null +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue @@ -0,0 +1,34 @@ +<script> +import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; +import SidebarTodo from './sidebar_todo.vue'; + +export default { + components: { + ToggleSidebar, + SidebarTodo, + }, + props: { + sidebarCollapsed: { + type: Boolean, + required: true, + }, + }, +}; +</script> + +<template> + <div class="block d-flex justify-content-between"> + <span class="issuable-header-text hide-collapsed"> + {{ __('Quick actions') }} + </span> + <toggle-sidebar + :collapsed="sidebarCollapsed" + css-classes="ml-auto" + @toggle="$emit('toggle-sidebar')" + /> + <!-- 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> + </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 new file mode 100644 index 00000000000..89dbbedd9c1 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue @@ -0,0 +1,189 @@ +<script> +import { + GlIcon, + GlDropdown, + GlDropdownItem, + GlLoadingIcon, + GlTooltip, + GlButton, + 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'; + +export default { + statuses: { + TRIGGERED: s__('AlertManagement|Triggered'), + ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), + RESOLVED: s__('AlertManagement|Resolved'), + }, + components: { + GlIcon, + GlDropdown, + GlDropdownItem, + GlLoadingIcon, + GlTooltip, + GlButton, + GlSprintf, + }, + props: { + projectPath: { + type: String, + required: true, + }, + alert: { + type: Object, + required: true, + }, + isEditable: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + isDropdownShowing: false, + isUpdating: false, + }; + }, + computed: { + dropdownClass() { + return this.isDropdownShowing ? 'show' : 'gl-display-none'; + }, + }, + methods: { + hideDropdown() { + this.isDropdownShowing = false; + }, + toggleFormDropdown() { + this.isDropdownShowing = !this.isDropdownShowing; + const { dropdown } = this.$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 }); + }, + }, +}; +</script> + +<template> + <div class="block alert-status"> + <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')"> + <gl-icon name="status" :size="14" /> + <gl-loading-icon v-if="isUpdating" /> + </div> + <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left"> + <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')"> + <template #status> + {{ alert.status.toLowerCase() }} + </template> + </gl-sprintf> + </gl-tooltip> + + <div class="hide-collapsed"> + <p class="title gl-display-flex justify-content-between"> + {{ s__('AlertManagement|Status') }} + <a + v-if="isEditable" + ref="editButton" + class="btn-link" + href="#" + @click="toggleFormDropdown" + @keydown.esc="hideDropdown" + > + {{ s__('AlertManagement|Edit') }} + </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> + + <gl-loading-icon v-if="isUpdating" :inline="true" /> + <p + v-else-if="!isDropdownShowing" + class="value gl-m-0" + :class="{ 'no-value': !$options.statuses[alert.status] }" + > + <span + v-if="$options.statuses[alert.status]" + class="gl-text-gray-700" + data-testid="status" + >{{ $options.statuses[alert.status] }}</span + > + <span v-else> + {{ s__('AlertManagement|None') }} + </span> + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue new file mode 100644 index 00000000000..87090165f82 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue @@ -0,0 +1,29 @@ +<script> +import Todo from '~/sidebar/components/todo_toggle/todo.vue'; + +export default { + components: { + Todo, + }, + props: { + sidebarCollapsed: { + type: Boolean, + required: true, + }, + }, +}; +</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 }"> + <todo + :collapsed="sidebarCollapsed" + :issuable-id="1" + :is-todo="false" + :is-action-active="false" + issuable-type="alert" + @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 new file mode 100644 index 00000000000..9042d51aecf --- /dev/null +++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue @@ -0,0 +1,46 @@ +<script> +import NoteHeader from '~/notes/components/note_header.vue'; +import { spriteIcon } from '~/lib/utils/common_utils'; + +export default { + components: { + NoteHeader, + }, + props: { + note: { + type: Object, + required: true, + }, + }, + computed: { + noteAnchorId() { + return `note_${this.note?.id?.split('/').pop()}`; + }, + noteAuthor() { + const { + author, + author: { id }, + } = this.note; + return { ...author, id: id?.split('/').pop() }; + }, + iconHtml() { + return spriteIcon('user'); + }, + }, +}; +</script> + +<template> + <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper"> + <div class="timeline-entry-inner"> + <div class="timeline-icon" v-html="iconHtml"></div> + <div class="timeline-content"> + <div class="note-header"> + <note-header :author="noteAuthor" :created-at="note.createdAt" :note-id="note.id"> + <span v-html="note.bodyHtml"></span> + </note-header> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js index 9df01d9d0b5..b9670466c0f 100644 --- a/app/assets/javascripts/alert_management/constants.js +++ b/app/assets/javascripts/alert_management/constants.js @@ -9,38 +9,59 @@ export const ALERTS_SEVERITY_LABELS = { UNKNOWN: s__('AlertManagement|Unknown'), }; -export const ALERTS_STATUS = { - OPEN: 'OPEN', - TRIGGERED: 'TRIGGERED', - ACKNOWLEDGED: 'ACKNOWLEDGED', - RESOLVED: 'RESOLVED', - ALL: 'ALL', -}; - export const ALERTS_STATUS_TABS = [ { title: s__('AlertManagement|Open'), - status: ALERTS_STATUS.OPEN, - filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED], + status: 'OPEN', + filters: ['TRIGGERED', 'ACKNOWLEDGED'], }, { title: s__('AlertManagement|Triggered'), - status: ALERTS_STATUS.TRIGGERED, - filters: [ALERTS_STATUS.TRIGGERED], + status: 'TRIGGERED', + filters: 'TRIGGERED', }, { title: s__('AlertManagement|Acknowledged'), - status: ALERTS_STATUS.ACKNOWLEDGED, - filters: [ALERTS_STATUS.ACKNOWLEDGED], + status: 'ACKNOWLEDGED', + filters: 'ACKNOWLEDGED', }, { title: s__('AlertManagement|Resolved'), - status: ALERTS_STATUS.RESOLVED, - filters: [ALERTS_STATUS.RESOLVED], + status: 'RESOLVED', + filters: 'RESOLVED', }, { title: s__('AlertManagement|All alerts'), - status: ALERTS_STATUS.ALL, - filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED, ALERTS_STATUS.RESOLVED], + status: 'ALL', + filters: ['TRIGGERED', 'ACKNOWLEDGED', 'RESOLVED'], }, ]; + +/* eslint-disable @gitlab/require-i18n-strings */ + +/** + * Tracks snowplow event when user views alerts list + */ +export const trackAlertListViewsOptions = { + category: 'Alert Management', + action: 'view_alerts_list', +}; + +/** + * Tracks snowplow event when user views alert details + */ +export const trackAlertsDetailsViewsOptions = { + category: 'Alert Management', + action: 'view_alert_details', +}; + +/** + * Tracks snowplow event when alert status is updated + */ +export const trackAlertStatusUpdateOptions = { + category: 'Alert Management', + action: 'update_alert_status', + label: 'Status', +}; + +export const DEFAULT_PAGE_SIZE = 10; diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js index d3523e0a29d..aa8a839ea3f 100644 --- a/app/assets/javascripts/alert_management/details.js +++ b/app/assets/javascripts/alert_management/details.js @@ -8,7 +8,7 @@ Vue.use(VueApollo); export default selector => { const domEl = document.querySelector(selector); - const { alertId, projectPath, newIssuePath } = domEl.dataset; + const { alertId, projectPath, projectIssuesPath } = domEl.dataset; const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( @@ -39,7 +39,7 @@ export default selector => { props: { alertId, projectPath, - newIssuePath, + projectIssuesPath, }, }); }, diff --git a/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql new file mode 100644 index 00000000000..c72300e9757 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql @@ -0,0 +1,16 @@ +#import "~/graphql_shared/fragments/author.fragment.graphql" + +fragment AlertNote on Note { + id + author { + id + state + ...Author + } + body + bodyHtml + createdAt + discussion { + id + } +} diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql index df802616e97..cbe7e169be3 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql +++ b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql @@ -1,4 +1,5 @@ -#import "./listItem.fragment.graphql" +#import "./list_item.fragment.graphql" +#import "./alert_note.fragment.graphql" fragment AlertDetailItem on AlertManagementAlert { ...AlertListItem @@ -8,4 +9,9 @@ fragment AlertDetailItem on AlertManagementAlert { description updatedAt details + notes { + nodes { + ...AlertNote + } + } } diff --git a/app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql index fffe07b0cfd..746c4435f38 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql +++ b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql @@ -6,4 +6,10 @@ fragment AlertListItem on AlertManagementAlert { startedAt endedAt eventCount + issueIid + assignees { + nodes { + username + } + } } diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql new file mode 100644 index 00000000000..efeaf8fa372 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql @@ -0,0 +1,15 @@ +mutation($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) { + alertSetAssignees( + input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath } + ) { + errors + alert { + iid + assignees { + nodes { + username + } + } + } + } +} diff --git a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql new file mode 100644 index 00000000000..664596ab88f --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql @@ -0,0 +1,8 @@ +mutation ($projectPath: ID!, $iid: String!) { + createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) { + errors + issue { + iid + } + } +} diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql index 009ae0b2930..09151f233f5 100644 --- a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql +++ b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql @@ -4,6 +4,7 @@ mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) { alert { iid, status, + endedAt } } } diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql index 7c77715fad2..c02b8accdd1 100644 --- a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql +++ b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql @@ -1,4 +1,4 @@ -#import "../fragments/detailItem.fragment.graphql" +#import "../fragments/detail_item.fragment.graphql" query alertDetails($fullPath: ID!, $alertId: String) { project(fullPath: $fullPath) { diff --git a/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql deleted file mode 100644 index 54b66389d5b..00000000000 --- a/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql +++ /dev/null @@ -1,11 +0,0 @@ -#import "../fragments/listItem.fragment.graphql" - -query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!]) { - project(fullPath: $projectPath) { - alertManagementAlerts(statuses: $statuses) { - nodes { - ...AlertListItem - } - } - } -} diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql new file mode 100644 index 00000000000..1d3c3c83cc1 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql @@ -0,0 +1,32 @@ +#import "../fragments/list_item.fragment.graphql" + +query getAlerts( + $projectPath: ID!, + $statuses: [AlertManagementStatus!], + $sort: AlertManagementAlertSort, + $firstPageSize: Int, + $lastPageSize: Int, + $prevPageCursor: String = "" + $nextPageCursor: String = "" +) { + project(fullPath: $projectPath, ) { + alertManagementAlerts( + statuses: $statuses, + sort: $sort, + first: $firstPageSize + last: $lastPageSize, + after: $nextPageCursor, + before: $prevPageCursor + ) { + nodes { + ...AlertListItem + }, + pageInfo { + hasNextPage + endCursor + hasPreviousPage + startCursor + } + } + } +} diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql new file mode 100644 index 00000000000..1143050200c --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql @@ -0,0 +1,11 @@ +query getAlertsCount($projectPath: ID!) { + project(fullPath: $projectPath) { + alertManagementAlertStatusCounts { + all + open + acknowledged + resolved + triggered + } + } +} diff --git a/app/assets/javascripts/alert_management/services/index.js b/app/assets/javascripts/alert_management/services/index.js deleted file mode 100644 index 787603d3e7a..00000000000 --- a/app/assets/javascripts/alert_management/services/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -export default { - getAlertManagementList({ endpoint }) { - return axios.get(endpoint); - }, -}; |