Compare commits

...

1 Commits

Author SHA1 Message Date
Sylvère A.
6510716ef8 feat(web-core): add an optional details log viewer to the error modal 2026-10-06 12:48:39 +02:00
7 changed files with 160 additions and 22 deletions

View File

@@ -1,8 +1,8 @@
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkDeleteJob } from '@/modules/network/jobs/network-delete.job.ts'
import { useDeleteModal } from '@core/composables/modals/use-delete-modal.ts'
import { useErrorModal } from '@core/composables/modals/use-error-modal.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -23,12 +23,7 @@ export function useNetworkDelete(rawNetworks: MaybeRefOrGetter<XenApiNetwork[]>)
const { open: openNetworkDeleteModal } = useDeleteModal()
const { open: openNetworkDeleteErrorModal } = useOverlay({
component: () => import('@core/components/modal/VtsErrorModal.vue'),
events: {
onClose: true,
},
})
const { open: openNetworkDeleteErrorModal } = useErrorModal()
function deleteNetworks() {
if (!canDeleteNetworks.value) {

View File

@@ -408,6 +408,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/story/web-core/modal/vts-error-modal': RouteRecordInfo<
'/story/web-core/modal/vts-error-modal',
'/story/web-core/modal/vts-error-modal',
Record<never, never>,
Record<never, never>,
| never
>,
'/story/web-core/object-icon/vts-object-icon': RouteRecordInfo<
'/story/web-core/object-icon/vts-object-icon',
'/story/web-core/object-icon/vts-object-icon',
@@ -1421,6 +1428,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/stories/web-core/modal/vts-error-modal.story.vue': {
routes:
| '/story/web-core/modal/vts-error-modal'
views:
| never
}
'src/stories/web-core/object-icon/vts-object-icon.story.vue': {
routes:
| '/story/web-core/object-icon/vts-object-icon'

View File

@@ -0,0 +1,29 @@
# Usage
Open the modal with `useErrorModal`.
```vue-script
import { useErrorModal } from '@core/composables/modals/use-error-modal.ts'
const { open: openErrorModal } = useErrorModal()
try {
// ...
} catch (error) {
void openErrorModal({
props: {
title: 'Unable to create cluster',
error: 'An error occurred',
details: error,
},
})
}
```
The `details` prop is displayed below the message, even when the `content` slot is overridden:
```vue-template
<VtsErrorModal title="Unable to delete network" :details @close="close()">
<template #content>Custom error message</template>
</VtsErrorModal>
```

View File

@@ -0,0 +1,60 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('title').str().required().preset('Unable to create cluster').widget(),
prop('error').str().preset('An error occurred').widget(),
prop('details')
.type('string | object')
.preset(details1)
.widget(object())
.help('When set, displayed in a log entry viewer below the error message'),
event('close'),
slot('content').help('Replaces the error message. The details are still displayed below it'),
setting('contentSlotContent').widget(text()).help('Leave empty to use the error prop'),
]"
:presets
>
<VtsErrorModal class="story" v-bind="properties">
<template v-if="settings.contentSlotContent" #content>{{ settings.contentSlotContent }}</template>
</VtsErrorModal>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { event, prop, setting, slot } from '@/libs/story/story-param.ts'
import { object, text } from '@/libs/story/story-widget.ts'
import VtsErrorModal from '@core/components/modal/VtsErrorModal.vue'
const details1 = {
code: 'HOST_NOT_ENOUGH_FREE_MEMORY',
params: ['8589934592', '2147483648'],
call: {
method: 'VM.start',
params: ['OpaqueRef:3f2a9c1e-7b4d-4e8a-9f6c-2d1b5a8e7c90', false, false],
},
}
const details2 =
'TypeError: Failed to fetch\n at fetchApi (rest-api.ts:42:11)\n at async createCluster (cluster-create.job.ts:88:5)'
const presets = {
'Without details': {
props: { details: undefined },
},
'With details object': {
props: { details: details1 },
},
'With details string': {
props: { details: details2 },
},
}
</script>
<style lang="postcss" scoped>
.story {
position: relative;
height: 100%;
}
</style>

View File

@@ -1,13 +1,29 @@
<template>
<UiModal accent="danger" icon="status:danger-picto" @dismiss="emit('close')" @confirm="emit('close')">
<UiModal
class="vts-error-modal"
accent="danger"
icon="status:info-picto"
@dismiss="emit('close')"
@confirm="emit('close')"
>
<template #title>
{{ title }}
</template>
<template #content>
<slot name="content">
{{ error }}
</slot>
<div class="content">
<slot name="content">
{{ error }}
</slot>
<UiLogEntryViewer
v-if="details !== undefined"
:label="t('see-details')"
:content="details"
accent="danger"
size="small"
class="details"
/>
</div>
</template>
<template #buttons>
@@ -20,12 +36,14 @@
<script lang="ts" setup>
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
import UiModal from '@core/components/ui/modal/UiModal.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
title: string
error?: string
details?: string | object
}>()
const emit = defineEmits<{
@@ -38,3 +56,19 @@ defineSlots<{
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.vts-error-modal {
.content {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.details {
text-align: start;
/* The small action buttons must not overflow the scrollable modal content */
overflow-x: clip;
}
}
</style>

View File

@@ -0,0 +1,10 @@
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
export function useErrorModal() {
return useOverlay({
component: () => import('@core/components/modal/VtsErrorModal.vue'),
events: {
onClose: true,
},
})
}

View File

@@ -1,17 +1,14 @@
<template>
<VtsErrorModal :title="t('unable-to-delete-network')" @close="emit('close')">
<template #content>
{{ error ?? t('error') }}
<template v-if="showConnectedVifsMessage">
<br />
<I18nT keypath="disconnect-vifs-in-xo-5-to-delete-network" scope="global">
<template #xo-5>
<UiLink :href="xo5Link" size="medium">
{{ t('xo-5') }}
</UiLink>
</template>
</I18nT>
</template>
<p>{{ error ?? t('error') }}</p>
<I18nT v-if="showConnectedVifsMessage" keypath="disconnect-vifs-in-xo-5-to-delete-network" scope="global" tag="p">
<template #xo-5>
<UiLink :href="xo5Link" size="medium">
{{ t('xo-5') }}
</UiLink>
</template>
</I18nT>
</template>
</VtsErrorModal>
</template>