Improve error handling for app extensions (#17191)

* add util function to get vue component name

* add global error handler

* add v-error-boundary component

* use error boundary to wrap insights panels

* use error boundary to wrap form interfaces

* use error boundary in render display and template

* use error boundary in extension options

* use error boundary for flows operation overview

* extract default options-overview into a component

* add tests
This commit is contained in:
Azri Kahar
2023-03-16 20:04:17 +08:00
committed by GitHub
parent 4d7e81f295
commit eb65d60236
14 changed files with 372 additions and 112 deletions

View File

@@ -121,18 +121,27 @@
>
{{ t('no_data') }}
</div>
<component
:is="`panel-${tile.data.type}`"
v-else
v-bind="tile.data.options"
:id="tile.id"
:dashboard="primaryKey"
:show-header="tile.showHeader"
:height="tile.height"
:width="tile.width"
:now="now"
:data="data[tile.id]"
/>
<v-error-boundary v-else :name="`panel-${tile.data.type}`">
<component
:is="`panel-${tile.data.type}`"
v-bind="tile.data.options"
:id="tile.id"
:dashboard="primaryKey"
:show-header="tile.showHeader"
:height="tile.height"
:width="tile.width"
:now="now"
:data="data[tile.id]"
/>
<template #fallback="{ error }">
<div class="panel-error">
<v-icon name="warning" />
{{ t('unexpected_error') }}
<v-error :error="error" />
</div>
</template>
</v-error-boundary>
</div>
</template>
</v-workspace>

View File

@@ -14,14 +14,18 @@
primary-key="+"
/>
<component
:is="`${type}-options-${extensionInfo!.id}`"
v-else
:value="optionsValues"
:collection="collection"
:field="field"
@input="optionsValues = $event"
/>
<v-error-boundary v-else :name="`${type}-options-${extensionInfo!.id}`">
<component
:is="`${type}-options-${extensionInfo!.id}`"
:value="optionsValues"
:collection="collection"
:field="field"
@input="optionsValues = $event"
/>
<template #fallback>
<v-notice type="warning">{{ t('unexpected_error') }}</v-notice>
</template>
</v-error-boundary>
</template>
<script lang="ts">

View File

@@ -79,33 +79,36 @@
<v-icon name="adjust" />
</div>
</template>
<div v-if="typeof currentOperation?.overview === 'function'" class="block">
<div v-tooltip="panel.key" class="name">
{{ panel.id === '$trigger' ? t(`triggers.${panel.type}.name`) : panel.name }}
<v-error-boundary
v-if="typeof currentOperation?.overview === 'function'"
:name="`operation-overview-${currentOperation.id}`"
>
<div class="block">
<options-overview :panel="panel" :current-operation="currentOperation" :flow="flow" />
</div>
<dl class="options-overview selectable">
<div
v-for="{ label, text, copyable } of translate(currentOperation?.overview(panel.options ?? {}, { flow }))"
:key="label"
>
<dt>{{ label }}</dt>
<dd>{{ text }}</dd>
<v-icon
v-if="isCopySupported && copyable"
name="copy"
small
clickable
class="clipboard-icon"
@click="copyToClipboard(text)"
/>
<template #fallback="{ error: optionsOverviewError }">
<div class="options-overview-error">
<v-icon name="warning" />
{{ t('unexpected_error') }}
<v-error :error="optionsOverviewError" />
</div>
</dl>
</div>
<component
:is="`operation-overview-${currentOperation.id}`"
</template>
</v-error-boundary>
<v-error-boundary
v-else-if="currentOperation && 'id' in currentOperation"
:options="currentOperation"
/>
:name="`operation-overview-${currentOperation.id}`"
>
<component :is="`operation-overview-${currentOperation.id}`" :options="currentOperation" />
<template #fallback="{ error: operationOverviewError }">
<div class="options-overview-error">
<v-icon name="warning" />
{{ t('unexpected_error') }}
<v-error :error="operationOverviewError" />
</div>
</template>
</v-error-boundary>
<template v-if="panel.id === '$trigger'" #footer>
<div class="status-footer" :class="flowStatus">
<display-color
@@ -135,15 +138,14 @@
</template>
<script lang="ts" setup>
import { useClipboard } from '@/composables/use-clipboard';
import { useExtensions } from '@/extensions';
import { translate } from '@/utils/translate-object-values';
import { Vector2 } from '@/utils/vector2';
import { FlowRaw } from '@directus/shared/types';
import { computed, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
import { ATTACHMENT_OFFSET, REJECT_OFFSET, RESOLVE_OFFSET } from '../constants';
import { getTriggers } from '../triggers';
import OptionsOverview from './options-overview.vue';
export type Target = 'resolve' | 'reject';
export type ArrowInfo = {
@@ -194,8 +196,6 @@ const emit = defineEmits([
const { t } = useI18n();
const { isCopySupported, copyToClipboard } = useClipboard();
const styleVars = {
'--reject-left': REJECT_OFFSET.x + 'px',
'--reject-top': REJECT_OFFSET.y + 'px',
@@ -483,27 +483,20 @@ function pointerLeave() {
}
}
.options-overview {
> div {
flex-wrap: wrap;
align-items: center;
margin-bottom: 6px;
}
.options-overview-error {
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
height: 100%;
dt {
flex-basis: 100%;
margin-bottom: -2px;
}
--v-icon-color: var(--danger);
dd {
font-family: var(--family-monospace);
flex-basis: 0;
}
.clipboard-icon {
--v-icon-color: var(--foreground-subdued);
--v-icon-color-hover: var(--foreground-normal);
margin-left: 4px;
.v-error {
margin-top: 8px;
max-width: 100%;
}
}

View File

@@ -0,0 +1,65 @@
<template>
<div v-tooltip="panel.key" class="name">
{{ panel.id === '$trigger' ? t(`triggers.${panel.type}.name`) : panel.name }}
</div>
<dl class="options-overview selectable">
<div
v-for="{ label, text, copyable } of translate(currentOperation?.overview(panel.options ?? {}, { flow }))"
:key="label"
>
<dt>{{ label }}</dt>
<dd>{{ text }}</dd>
<v-icon
v-if="isCopySupported && copyable"
name="copy"
small
clickable
class="clipboard-icon"
@click="copyToClipboard(text)"
/>
</div>
</dl>
</template>
<script setup lang="ts">
import { useClipboard } from '@/composables/use-clipboard';
import { translate } from '@/utils/translate-object-values';
import { FlowRaw } from '@directus/shared/types';
import { useI18n } from 'vue-i18n';
defineProps<{
panel: Record<string, any>;
currentOperation: any;
flow: FlowRaw;
}>();
const { t } = useI18n();
const { isCopySupported, copyToClipboard } = useClipboard();
</script>
<style lang="scss" scoped>
.options-overview {
> div {
flex-wrap: wrap;
align-items: center;
margin-bottom: 6px;
}
dt {
flex-basis: 100%;
margin-bottom: -2px;
}
dd {
font-family: var(--family-monospace);
flex-basis: 0;
}
.clipboard-icon {
--v-icon-color: var(--foreground-subdued);
--v-icon-color-hover: var(--foreground-normal);
margin-left: 4px;
}
}
</style>