mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
1 Commits
326c152fac
...
fix/xo6-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee1f7f1b1 |
@@ -1089,10 +1089,12 @@
|
||||
"plugin:configuration-of": "the configuration of {name}",
|
||||
"plugin:configuration-presets": "Configuration presets",
|
||||
"plugin:configuration-presets-info": "Use a predefined configuration to fill the form",
|
||||
"plugin:configure-error": "The configuration couldn't be saved",
|
||||
"plugin:delete-configuration-info": "The configuration of the plugin will be lost.",
|
||||
"plugin:needs-configuration": "This plugin can't be loaded until it is configured.",
|
||||
"plugin:needs-configuration-short": "Needs a configuration before it can be loaded.",
|
||||
"plugin:not-configurable": "This plugin is not configurable",
|
||||
"plugin:test-error": "The test failed",
|
||||
"plugin:test-info": "Test the plugin with its saved configuration.",
|
||||
"plugins": "Plugins",
|
||||
"policy": "Policy",
|
||||
|
||||
@@ -1089,10 +1089,12 @@
|
||||
"plugin:configuration-of": "la configuration de {name}",
|
||||
"plugin:configuration-presets": "Configurations prédéfinies",
|
||||
"plugin:configuration-presets-info": "Utilisez une configuration prédéfinie pour remplir le formulaire",
|
||||
"plugin:configure-error": "La configuration n'a pas pu être enregistrée",
|
||||
"plugin:delete-configuration-info": "La configuration du plugin sera perdue.",
|
||||
"plugin:needs-configuration": "Ce plugin ne peut pas être chargé tant qu'il n'est pas configuré.",
|
||||
"plugin:needs-configuration-short": "Nécessite une configuration avant de pouvoir être chargé.",
|
||||
"plugin:not-configurable": "Ce plugin n'est pas configurable",
|
||||
"plugin:test-error": "Le test a échoué",
|
||||
"plugin:test-info": "Testez le plugin avec sa configuration enregistrée.",
|
||||
"plugins": "Plugins",
|
||||
"policy": "Politique",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PluginConfigurationDrawer from '@/modules/plugin/components/drawer/PluginConfigurationDrawer.vue'
|
||||
import PluginRunError from '@/modules/plugin/components/drawer/PluginRunError.vue'
|
||||
import JsonSchemaField from '@/modules/plugin/components/json-schema-form/JsonSchemaField.vue'
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
import type { JsonSchema } from '@/modules/plugin/utils/json-schema.util.ts'
|
||||
@@ -28,9 +29,9 @@ const GMAIL_PRESET = { from: { address: 'xo@gmail.com' }, transport: { host: 'sm
|
||||
|
||||
const configuredPlugin = createPlugin({ configurationSchema: SCHEMA, configuration: SAVED_CONFIGURATION })
|
||||
|
||||
function mountDrawer(plugin: FrontXoPlugin = configuredPlugin) {
|
||||
function mountDrawer(plugin: FrontXoPlugin = configuredPlugin, error?: Error) {
|
||||
return mount(PluginConfigurationDrawer, {
|
||||
props: { plugin },
|
||||
props: { plugin, error },
|
||||
global: createGlobalTestConfig(),
|
||||
attachTo: document.body,
|
||||
})
|
||||
@@ -99,3 +100,16 @@ it('lists the missing fields instead of confirming', async () => {
|
||||
expect(wrapper.emitted('confirm')).toBeUndefined()
|
||||
expect(wrapper.getComponent(VtsFormErrorSummary).props('errors')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('shows no run error before a run failed', () => {
|
||||
expect(mountDrawer().findComponent(PluginRunError).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the error of the failed run', () => {
|
||||
const error = new Error('SMTP connection refused')
|
||||
|
||||
expect(mountDrawer(undefined, error).getComponent(PluginRunError).props()).toEqual({
|
||||
title: t('plugin:configure-error'),
|
||||
error,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
</UiButton>
|
||||
</div>
|
||||
</VtsInputWrapper>
|
||||
<PluginRunError v-if="error !== undefined" :title="t('plugin:configure-error')" :error />
|
||||
<VtsFormErrorSummary :errors="summaryErrors" />
|
||||
<JsonSchemaField v-if="schema !== undefined" v-model="formValue" :schema :label="t('configuration')" required />
|
||||
</template>
|
||||
@@ -31,6 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PluginRunError from '@/modules/plugin/components/drawer/PluginRunError.vue'
|
||||
import JsonSchemaField from '@/modules/plugin/components/json-schema-form/JsonSchemaField.vue'
|
||||
import { useXoPluginConfigurationForm } from '@/modules/plugin/composables/use-xo-plugin-configuration-form.composable.ts'
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
@@ -45,6 +47,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { plugin } = defineProps<{
|
||||
plugin: FrontXoPlugin
|
||||
error?: Error
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import PluginRunError from '@/modules/plugin/components/drawer/PluginRunError.vue'
|
||||
import { ApiError } from '@/shared/error/api.error.ts'
|
||||
import { createGlobalTestConfig } from '@/test/global-test-config.ts'
|
||||
import { t } from '@/test/i18n.ts'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const TITLE = "The configuration couldn't be saved"
|
||||
|
||||
// an error of a plugin, as the REST API answers it
|
||||
const PLUGIN_ERROR = { error: 'no monitor is configured' }
|
||||
|
||||
// a validation error, as the REST API answers it: its `error` is not a message
|
||||
const VALIDATION_ERROR = {
|
||||
error: { body: { message: 'Value does not match any allowed schema', value: ['not an object'] } },
|
||||
data: {},
|
||||
}
|
||||
|
||||
function mountError(error: Error) {
|
||||
return mount(PluginRunError, {
|
||||
props: { title: TITLE, error },
|
||||
global: createGlobalTestConfig(),
|
||||
})
|
||||
}
|
||||
|
||||
it('shows the message of the server, not the HTTP status text', () => {
|
||||
const alert = mountError(new ApiError('Internal Server Error', { status: 500, cause: PLUGIN_ERROR })).getComponent(
|
||||
UiAlert
|
||||
)
|
||||
|
||||
expect(alert.props('accent')).toBe('danger')
|
||||
expect(alert.text()).toContain(TITLE)
|
||||
expect(alert.text()).toContain('no monitor is configured')
|
||||
expect(alert.text()).not.toContain('Internal Server Error')
|
||||
})
|
||||
|
||||
it('falls back to the HTTP status text when the server sent no message', () => {
|
||||
expect(
|
||||
mountError(new ApiError('Unprocessable Entity', { status: 422, cause: VALIDATION_ERROR }))
|
||||
.getComponent(UiAlert)
|
||||
.text()
|
||||
).toContain('Unprocessable Entity')
|
||||
expect(
|
||||
mountError(new ApiError('Bad Gateway', { status: 502 }))
|
||||
.getComponent(UiAlert)
|
||||
.text()
|
||||
).toContain('Bad Gateway')
|
||||
})
|
||||
|
||||
it('shows the details of the server error', () => {
|
||||
const viewer = mountError(
|
||||
new ApiError('Unprocessable Entity', { status: 422, cause: VALIDATION_ERROR })
|
||||
).getComponent(UiLogEntryViewer)
|
||||
|
||||
expect(viewer.props()).toMatchObject({ label: t('api-error-details'), accent: 'danger', content: VALIDATION_ERROR })
|
||||
})
|
||||
|
||||
it('shows the message of an error that did not come from the server, with no details', () => {
|
||||
const wrapper = mountError(new Error('Failed to fetch'))
|
||||
|
||||
expect(wrapper.getComponent(UiAlert).text()).toContain('Failed to fetch')
|
||||
expect(wrapper.findComponent(UiLogEntryViewer).exists()).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="plugin-run-error">
|
||||
<UiAlert accent="danger">
|
||||
{{ title }}
|
||||
<template #description>{{ message }}</template>
|
||||
</UiAlert>
|
||||
<UiLogEntryViewer
|
||||
v-if="details !== undefined"
|
||||
:label="t('api-error-details')"
|
||||
accent="danger"
|
||||
size="small"
|
||||
:content="details"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ApiError } from '@/shared/error/api.error.ts'
|
||||
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
|
||||
import UiLogEntryViewer from '@core/components/ui/log-entry-viewer/UiLogEntryViewer.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { error } = defineProps<{
|
||||
title: string
|
||||
error: Error
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// the REST API answers an error with `{ error, data?, info? }`: its message is more useful than the HTTP status text
|
||||
const message = computed(() => {
|
||||
const serverMessage = error instanceof ApiError ? error.cause?.error : undefined
|
||||
|
||||
return typeof serverMessage === 'string' ? serverMessage : error.message
|
||||
})
|
||||
|
||||
const details = computed(() => (error instanceof ApiError ? error.cause : undefined))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.plugin-run-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
margin-block: 0.8rem 2.4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,4 @@
|
||||
import PluginRunError from '@/modules/plugin/components/drawer/PluginRunError.vue'
|
||||
import PluginTestDrawer from '@/modules/plugin/components/drawer/PluginTestDrawer.vue'
|
||||
import JsonSchemaField from '@/modules/plugin/components/json-schema-form/JsonSchemaField.vue'
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
@@ -20,9 +21,9 @@ const REQUIRED_TEST_SCHEMA = {
|
||||
required: ['to'],
|
||||
} satisfies JsonSchema
|
||||
|
||||
function mountDrawer(plugin: FrontXoPlugin = createPlugin({ testable: true, testSchema: TEST_SCHEMA })) {
|
||||
function mountDrawer(plugin: FrontXoPlugin = createPlugin({ testable: true, testSchema: TEST_SCHEMA }), error?: Error) {
|
||||
return mount(PluginTestDrawer, {
|
||||
props: { plugin },
|
||||
props: { plugin, error },
|
||||
global: createGlobalTestConfig(),
|
||||
attachTo: document.body,
|
||||
})
|
||||
@@ -84,3 +85,16 @@ it('lists the missing fields instead of confirming', async () => {
|
||||
expect(wrapper.emitted('confirm')).toBeUndefined()
|
||||
expect(wrapper.getComponent(VtsFormErrorSummary).props('errors')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('shows no run error before a run failed', () => {
|
||||
expect(mountDrawer().findComponent(PluginRunError).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the error of the failed run', () => {
|
||||
const error = new Error('SMTP connection refused')
|
||||
|
||||
expect(mountDrawer(undefined, error).getComponent(PluginRunError).props()).toEqual({
|
||||
title: t('plugin:test-error'),
|
||||
error,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<template #content>
|
||||
<UiInfo accent="info" wrap>{{ t('plugin:test-info') }}</UiInfo>
|
||||
<PluginRunError v-if="error !== undefined" :title="t('plugin:test-error')" :error />
|
||||
<VtsFormErrorSummary :errors="summaryErrors" />
|
||||
<JsonSchemaField
|
||||
v-if="testSchema !== undefined"
|
||||
@@ -26,6 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PluginRunError from '@/modules/plugin/components/drawer/PluginRunError.vue'
|
||||
import JsonSchemaField from '@/modules/plugin/components/json-schema-form/JsonSchemaField.vue'
|
||||
import { useXoPluginTestForm } from '@/modules/plugin/composables/use-xo-plugin-test-form.composable.ts'
|
||||
import type { FrontXoPlugin } from '@/modules/plugin/remote-resources/use-xo-plugin-collection.ts'
|
||||
@@ -38,6 +40,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { plugin } = defineProps<{
|
||||
plugin: FrontXoPlugin
|
||||
error?: Error
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -25,17 +25,23 @@ export function useXoPluginDrawerJob(
|
||||
|
||||
const { run, canRun, isRunning, errorMessage } = useJob(plugin, payload)
|
||||
|
||||
// the error of the last run, shown in the drawer, which stays open
|
||||
const runError = ref<Error>()
|
||||
|
||||
const { open } = useOverlay({
|
||||
component: drawer,
|
||||
events: {
|
||||
onConfirm: async (newPayload: unknown) => {
|
||||
payload.value = newPayload
|
||||
runError.value = undefined
|
||||
|
||||
try {
|
||||
await run()
|
||||
} catch (error) {
|
||||
console.error(`Error when running a job on the plugin ${plugin.value.name}`, error)
|
||||
|
||||
runError.value = error instanceof Error ? error : new Error(String(error))
|
||||
|
||||
return KEEP_OVERLAY_OPEN
|
||||
}
|
||||
},
|
||||
@@ -45,9 +51,12 @@ export function useXoPluginDrawerJob(
|
||||
|
||||
const props = reactiveComputed(() => ({
|
||||
plugin: plugin.value,
|
||||
error: runError.value,
|
||||
}))
|
||||
|
||||
function openDrawer() {
|
||||
runError.value = undefined
|
||||
|
||||
return open({ props })
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ function getOverlayOptions(): OverlayOptions {
|
||||
return useOverlayMock.mock.calls[0][0]
|
||||
}
|
||||
|
||||
function getDrawerProps(): { plugin: FrontXoPlugin; error?: Error } {
|
||||
return open.mock.lastCall![0].props
|
||||
}
|
||||
|
||||
function getJobArgs() {
|
||||
const [plugin, payload] = useJob.mock.calls[0] as [Ref<FrontXoPlugin>, Ref<unknown>]
|
||||
|
||||
@@ -115,4 +119,36 @@ describe('onConfirm', () => {
|
||||
|
||||
expect(response).toBe(KEEP_OVERLAY_OPEN)
|
||||
})
|
||||
|
||||
it('gives the error of the failed job to the drawer', async () => {
|
||||
const error = new Error('SMTP connection refused')
|
||||
run.mockRejectedValue(error)
|
||||
mountDrawerJob().openDrawer()
|
||||
|
||||
await getOverlayOptions().events.onConfirm(PAYLOAD)
|
||||
|
||||
expect(getDrawerProps().error).toBe(error)
|
||||
})
|
||||
|
||||
it('clears the error when the job is run again', async () => {
|
||||
run.mockRejectedValueOnce(new Error('SMTP connection refused'))
|
||||
mountDrawerJob().openDrawer()
|
||||
await getOverlayOptions().events.onConfirm(PAYLOAD)
|
||||
|
||||
run.mockImplementationOnce(async () => expect(getDrawerProps().error).toBeUndefined())
|
||||
await getOverlayOptions().events.onConfirm(PAYLOAD)
|
||||
|
||||
expect(run).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('clears the error when the drawer is opened again', async () => {
|
||||
run.mockRejectedValue(new Error('SMTP connection refused'))
|
||||
const drawerJob = mountDrawerJob()
|
||||
drawerJob.openDrawer()
|
||||
await getOverlayOptions().events.onConfirm(PAYLOAD)
|
||||
|
||||
drawerJob.openDrawer()
|
||||
|
||||
expect(getDrawerProps().error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
> Users must be able to say: "I had this issue, happy to know it's fixed"
|
||||
|
||||
- [XO6/Pools] Pool side panel now shows the same pool name, UUID and icon as the pools table, and keeps the pool card when the pool is disconnected or unreachable (PR [#10714](https://github.com/vatesfr/xen-orchestra/pull/10714))
|
||||
- [XO6/Plugins] When saving a plugin's configuration or testing a plugin fails, the drawer now shows why instead of staying open without any message (PR [#10736](https://github.com/vatesfr/xen-orchestra/pull/10736))
|
||||
|
||||
### Packages to release
|
||||
|
||||
|
||||
Reference in New Issue
Block a user