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