summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/alert_management
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/alert_management')
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue262
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue216
-rw-r--r--app/assets/javascripts/alert_management/components/alert_sidebar.vue61
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue51
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue278
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue34
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue189
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue29
-rw-r--r--app/assets/javascripts/alert_management/components/system_notes/system_note.vue46
-rw-r--r--app/assets/javascripts/alert_management/constants.js57
-rw-r--r--app/assets/javascripts/alert_management/details.js4
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql16
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql (renamed from app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql)8
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql (renamed from app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql)6
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.graphql15
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.graphql8
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql1
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/details.query.graphql2
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql32
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql11
-rw-r--r--app/assets/javascripts/alert_management/services/index.js7
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">&bull;</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);
- },
-};