mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
+5![github-actions[bot]](https://secure.gravatar.com/avatar/cb7b7994e594270ea25948a7bf186123?d=identicon&s=40)








ae22a74e62
#11720 Design at: https://github.com/openobserve/designs/blob/main/dashboards/ux-improvement-dialog/design.md --------- --------- Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com> Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com> Co-authored-by: Sai Nikhil <nikhil@openobserve.ai> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: ktx-akshay <akshay.patil@kiara.tech> Co-authored-by: Bhargav <BJP232004@GMAIL.COM> Co-authored-by: ktx-mihir <mihir.patel@kiara.tech> Co-authored-by: ktx-riya <riya.kukadiya@kiara.tech> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com> Co-authored-by: omkark06 <omkar@zinclabs.io> Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech> Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech> Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
618 lines
15 KiB
Vue
618 lines
15 KiB
Vue
<template>
|
|
<div v-if="visible" class="confirmation-overlay">
|
|
<div
|
|
class="confirmation-dialog"
|
|
:class="theme === 'dark' ? 'dark-mode' : 'light-mode'"
|
|
@keydown="handleDialogKeydown"
|
|
@click="handleDialogClick"
|
|
>
|
|
<div class="confirmation-header">
|
|
<OIcon name="help-outline" size="md" class="confirmation-icon" />
|
|
<span class="confirmation-title">{{ formattedMessage }}</span>
|
|
</div>
|
|
|
|
<div class="confirmation-buttons">
|
|
<!-- For navigation actions, show 3 buttons -->
|
|
<template v-if="isNavigationAction">
|
|
<OButton
|
|
ref="yesButtonRef"
|
|
variant="outline"
|
|
:block="true"
|
|
class="confirmation-btn confirm-btn"
|
|
:class="{ 'btn-focused': isFocusedYes }"
|
|
tabindex="0"
|
|
@click="handleConfirm"
|
|
@focus="handleYesFocus"
|
|
@blur="handleYesBlur"
|
|
>Allow</OButton>
|
|
<OButton
|
|
ref="alwaysButtonRef"
|
|
variant="outline"
|
|
:block="true"
|
|
class="confirmation-btn always-btn"
|
|
:class="{ 'btn-focused': isFocusedAlways }"
|
|
tabindex="1"
|
|
@click="handleAlwaysConfirm"
|
|
@focus="handleAlwaysFocus"
|
|
@blur="handleAlwaysBlur"
|
|
>Always Allow</OButton>
|
|
<OButton
|
|
ref="noButtonRef"
|
|
variant="outline"
|
|
:block="true"
|
|
class="confirmation-btn cancel-btn"
|
|
:class="{ 'btn-focused': isFocusedNo }"
|
|
tabindex="2"
|
|
@click="handleCancel"
|
|
@focus="handleNoFocus"
|
|
@blur="handleNoBlur"
|
|
>No</OButton>
|
|
</template>
|
|
|
|
<!-- For other actions, show 2 buttons -->
|
|
<template v-else>
|
|
<OButton
|
|
ref="yesButtonRef"
|
|
variant="outline"
|
|
:block="true"
|
|
class="confirmation-btn confirm-btn"
|
|
:class="{ 'btn-focused': isFocusedYes }"
|
|
tabindex="0"
|
|
@click="handleConfirm"
|
|
@focus="handleYesFocus"
|
|
@blur="handleYesBlur"
|
|
>{{ confirmLabel }}</OButton>
|
|
<OButton
|
|
ref="noButtonRef"
|
|
variant="outline"
|
|
:block="true"
|
|
class="confirmation-btn cancel-btn"
|
|
:class="{ 'btn-focused': isFocusedNo }"
|
|
tabindex="1"
|
|
@click="handleCancel"
|
|
@focus="handleNoFocus"
|
|
@blur="handleNoBlur"
|
|
>{{ cancelLabel }}</OButton>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, nextTick, computed, onMounted, onUnmounted } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import OButton from '@/lib/core/Button/OButton.vue';
|
|
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
|
|
|
interface ConfirmationData {
|
|
tool?: string;
|
|
args?: Record<string, any>;
|
|
message?: string;
|
|
}
|
|
|
|
interface Props {
|
|
visible: boolean;
|
|
confirmation: ConfirmationData | null;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
confirmLabel: 'Yes',
|
|
cancelLabel: 'No',
|
|
confirmation: null,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
confirm: [];
|
|
cancel: [];
|
|
alwaysConfirm: [];
|
|
}>();
|
|
|
|
const store = useStore();
|
|
const theme = computed(() => store.state.theme);
|
|
|
|
// Check if this is a navigation action
|
|
const isNavigationAction = computed(() => props.confirmation?.tool === 'navigation_action');
|
|
|
|
// Format message based on confirmation data
|
|
const formattedMessage = computed(() => {
|
|
if (!props.confirmation) return '';
|
|
|
|
// Handle navigation_action
|
|
if (isNavigationAction.value) {
|
|
// Use the label/message from the navigation action
|
|
const message = props.confirmation.message;
|
|
if (message) {
|
|
return `Allow O2 Assistant to ${message}?`;
|
|
}
|
|
|
|
// Fallback: format based on target
|
|
const target = props.confirmation.args;
|
|
if (target?.name) {
|
|
return `Allow O2 Assistant to navigate to ${target.name}?`;
|
|
}
|
|
|
|
return 'Allow O2 Assistant to navigate?';
|
|
}
|
|
|
|
// Handle Delete* operations generically (DeleteAlert, DeleteDashboard, DeletePipeline, etc.)
|
|
if (props.confirmation.tool && props.confirmation.tool.startsWith('Delete')) {
|
|
// Extract entity type (e.g., "Alert" from "DeleteAlert")
|
|
const entityType = props.confirmation.tool.replace('Delete', '');
|
|
const entityTypeLower = entityType.toLowerCase();
|
|
const args = props.confirmation.args || {};
|
|
|
|
// Try to find a name or title for the entity
|
|
const name = args.name || args.title || args.alert_id || args.dashboard_id || args.pipeline_id || args.id;
|
|
|
|
if (name) {
|
|
return `Do you really want to delete the "${name}" ${entityTypeLower}?`;
|
|
}
|
|
|
|
// Fallback if no identifier found
|
|
return `Do you really want to delete this ${entityTypeLower}?`;
|
|
}
|
|
|
|
// Fallback to message property
|
|
return props.confirmation.message || '';
|
|
});
|
|
|
|
const yesButtonRef = ref<any>(null);
|
|
const noButtonRef = ref<any>(null);
|
|
const alwaysButtonRef = ref<any>(null);
|
|
const isFocusedYes = ref(false);
|
|
const isFocusedNo = ref(false);
|
|
const isFocusedAlways = ref(false);
|
|
|
|
// Auto-focus button when dialog appears
|
|
// For delete operations, focus "No" button by default to prevent accidental deletions
|
|
// For other operations, focus "Yes" button
|
|
watch(
|
|
() => props.visible,
|
|
(newValue) => {
|
|
if (newValue) {
|
|
nextTick(() => {
|
|
setTimeout(() => {
|
|
// Check if this is a delete operation
|
|
const isDeleteOperation = props.confirmation?.tool?.startsWith('Delete');
|
|
|
|
if (isDeleteOperation) {
|
|
// Focus "No" button for delete operations
|
|
const button = noButtonRef.value?.$el;
|
|
if (button) {
|
|
button.focus();
|
|
isFocusedNo.value = true;
|
|
}
|
|
} else {
|
|
// Focus "Yes" button for other operations
|
|
const button = yesButtonRef.value?.$el;
|
|
if (button) {
|
|
button.focus();
|
|
isFocusedYes.value = true;
|
|
}
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
const handleConfirm = () => {
|
|
emit('confirm');
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
emit('cancel');
|
|
};
|
|
|
|
const handleAlwaysConfirm = () => {
|
|
emit('alwaysConfirm');
|
|
};
|
|
|
|
const focusYes = () => {
|
|
const button = yesButtonRef.value?.$el;
|
|
if (button) {
|
|
button.focus();
|
|
}
|
|
};
|
|
|
|
const focusNo = () => {
|
|
const button = noButtonRef.value?.$el;
|
|
if (button) {
|
|
button.focus();
|
|
}
|
|
};
|
|
|
|
const focusAlways = () => {
|
|
const button = alwaysButtonRef.value?.$el;
|
|
if (button) {
|
|
button.focus();
|
|
}
|
|
};
|
|
|
|
const handleYesFocus = () => {
|
|
isFocusedYes.value = true;
|
|
isFocusedNo.value = false;
|
|
isFocusedAlways.value = false;
|
|
};
|
|
|
|
const handleYesBlur = () => {
|
|
isFocusedYes.value = false;
|
|
};
|
|
|
|
const handleNoFocus = () => {
|
|
isFocusedNo.value = true;
|
|
isFocusedYes.value = false;
|
|
isFocusedAlways.value = false;
|
|
};
|
|
|
|
const handleNoBlur = () => {
|
|
isFocusedNo.value = false;
|
|
};
|
|
|
|
const handleAlwaysFocus = () => {
|
|
isFocusedAlways.value = true;
|
|
isFocusedYes.value = false;
|
|
isFocusedNo.value = false;
|
|
};
|
|
|
|
const handleAlwaysBlur = () => {
|
|
isFocusedAlways.value = false;
|
|
};
|
|
|
|
const handleDialogClick = (event: MouseEvent) => {
|
|
// If click is not on a button, refocus the last focused button
|
|
const target = event.target as HTMLElement;
|
|
if (!target.closest('.confirmation-btn')) {
|
|
nextTick(() => {
|
|
if (isFocusedNo.value) {
|
|
focusNo();
|
|
} else if (isFocusedAlways.value) {
|
|
focusAlways();
|
|
} else {
|
|
focusYes();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDialogKeydown = (event: KeyboardEvent) => {
|
|
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
if (isFocusedYes.value) {
|
|
handleConfirm();
|
|
} else if (isFocusedAlways.value) {
|
|
handleAlwaysConfirm();
|
|
} else if (isFocusedNo.value) {
|
|
handleCancel();
|
|
}
|
|
} else if (event.key === 'ArrowDown' || event.key === 'Down') {
|
|
event.preventDefault();
|
|
if (isNavigationAction.value) {
|
|
// For navigation: Allow -> Always Allow -> No -> Allow
|
|
if (isFocusedYes.value) {
|
|
focusAlways();
|
|
} else if (isFocusedAlways.value) {
|
|
focusNo();
|
|
} else {
|
|
focusYes();
|
|
}
|
|
} else {
|
|
// For other actions: Yes -> No -> Yes
|
|
focusNo();
|
|
}
|
|
} else if (event.key === 'ArrowUp' || event.key === 'Up') {
|
|
event.preventDefault();
|
|
if (isNavigationAction.value) {
|
|
// For navigation: No -> Always Allow -> Allow -> No
|
|
if (isFocusedNo.value) {
|
|
focusAlways();
|
|
} else if (isFocusedAlways.value) {
|
|
focusYes();
|
|
} else {
|
|
focusNo();
|
|
}
|
|
} else {
|
|
// For other actions: No -> Yes -> No
|
|
focusYes();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Named handler references so they can be removed in onUnmounted
|
|
let yesBtnHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
let alwaysBtnHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
let noBtnHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
let yesBtnEl: HTMLElement | null = null;
|
|
let alwaysBtnEl: HTMLElement | null = null;
|
|
let noBtnEl: HTMLElement | null = null;
|
|
|
|
// Add native keyboard listeners to buttons
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
yesBtnEl = yesButtonRef.value?.$el;
|
|
noBtnEl = noButtonRef.value?.$el;
|
|
alwaysBtnEl = alwaysButtonRef.value?.$el;
|
|
|
|
if (yesBtnEl) {
|
|
yesBtnHandler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleConfirm();
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (isNavigationAction.value) {
|
|
focusAlways();
|
|
} else {
|
|
focusNo();
|
|
}
|
|
}
|
|
};
|
|
yesBtnEl.addEventListener('keydown', yesBtnHandler);
|
|
}
|
|
|
|
if (alwaysBtnEl) {
|
|
alwaysBtnHandler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleAlwaysConfirm();
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
focusNo();
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
focusYes();
|
|
}
|
|
};
|
|
alwaysBtnEl.addEventListener('keydown', alwaysBtnHandler);
|
|
}
|
|
|
|
if (noBtnEl) {
|
|
noBtnHandler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleCancel();
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (isNavigationAction.value) {
|
|
focusAlways();
|
|
} else {
|
|
focusYes();
|
|
}
|
|
}
|
|
};
|
|
noBtnEl.addEventListener('keydown', noBtnHandler);
|
|
}
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (yesBtnEl && yesBtnHandler) {
|
|
yesBtnEl.removeEventListener('keydown', yesBtnHandler);
|
|
}
|
|
if (alwaysBtnEl && alwaysBtnHandler) {
|
|
alwaysBtnEl.removeEventListener('keydown', alwaysBtnHandler);
|
|
}
|
|
if (noBtnEl && noBtnHandler) {
|
|
noBtnEl.removeEventListener('keydown', noBtnHandler);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.confirmation-overlay {
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
animation: slideUp 0.25s ease-out;
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.confirmation-dialog {
|
|
width: 100%;
|
|
padding: 16px 16px 14px 16px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
|
|
&.light-mode {
|
|
background: #ffffff;
|
|
border: 2px solid #e4e7ec;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
&.dark-mode {
|
|
background: #1e1e1e;
|
|
border: 2px solid #323232;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
}
|
|
|
|
.confirmation-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
|
|
.confirmation-icon {
|
|
flex-shrink: 0;
|
|
margin-top: 2px;
|
|
|
|
.light-mode & {
|
|
color: #6b7280;
|
|
}
|
|
.dark-mode & {
|
|
color: #9ca3af;
|
|
}
|
|
}
|
|
|
|
.confirmation-title {
|
|
flex: 1;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
line-height: 1.5;
|
|
|
|
.light-mode & {
|
|
color: #1f2937;
|
|
}
|
|
.dark-mode & {
|
|
color: #f3f4f6;
|
|
}
|
|
}
|
|
}
|
|
|
|
.confirmation-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding-top: 14px;
|
|
margin-top: 4px;
|
|
|
|
.light-mode & {
|
|
border-top: 1px solid #e5e7eb;
|
|
}
|
|
.dark-mode & {
|
|
border-top: 1px solid #374151;
|
|
}
|
|
|
|
.confirmation-btn {
|
|
width: 100%;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
// padding: 11px 20px;
|
|
border-radius: 6px;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
transition: all 0.2s ease;
|
|
|
|
&.confirm-btn {
|
|
.light-mode & {
|
|
color: var(--q-primary);
|
|
border: 2px solid #d1d5db;
|
|
background: #ffffff;
|
|
|
|
&:hover {
|
|
background: #eff6ff;
|
|
border-color: var(--q-primary);
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: var(--q-primary) !important;
|
|
border-color: var(--q-primary) !important;
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.4) !important;
|
|
}
|
|
}
|
|
|
|
.dark-mode & {
|
|
color: var(--q-primary);
|
|
border: 2px solid #4b5563;
|
|
background: transparent;
|
|
|
|
&:hover {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
border-color: var(--q-primary);
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: var(--q-primary) !important;
|
|
border-color: var(--q-primary) !important;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4) !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.always-btn {
|
|
.light-mode & {
|
|
color: #059669;
|
|
border: 2px solid #d1d5db;
|
|
background: #ffffff;
|
|
|
|
&:hover {
|
|
background: #f0fdf4;
|
|
border-color: #34d399;
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: #059669 !important;
|
|
border-color: #059669 !important;
|
|
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.4) !important;
|
|
}
|
|
}
|
|
|
|
.dark-mode & {
|
|
color: #34d399;
|
|
border: 2px solid #4b5563;
|
|
background: transparent;
|
|
|
|
&:hover {
|
|
background: rgba(5, 150, 105, 0.1);
|
|
border-color: #34d399;
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: #059669 !important;
|
|
border-color: #059669 !important;
|
|
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.4) !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.cancel-btn {
|
|
.light-mode & {
|
|
color: #374151;
|
|
border: 2px solid #d1d5db;
|
|
background: #ffffff;
|
|
|
|
&:hover {
|
|
background: #fef2f2;
|
|
border-color: #fca5a5;
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: #ef4444 !important;
|
|
border-color: #ef4444 !important;
|
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.4) !important;
|
|
}
|
|
}
|
|
|
|
.dark-mode & {
|
|
color: #e5e7eb;
|
|
border: 2px solid #4b5563;
|
|
background: transparent;
|
|
|
|
&:hover {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-color: #f87171;
|
|
}
|
|
|
|
&.btn-focused {
|
|
color: #ffffff !important;
|
|
background-color: #dc2626 !important;
|
|
border-color: #dc2626 !important;
|
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.4) !important;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|