summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/alert_management/components/sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/alert_management/components/sidebar')
-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
5 files changed, 581 insertions, 0 deletions
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>