mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
feat(frontend): app on error (#1556)
* feat(frontend): app on error * feat(frontend): Add default onError errorOverlay + add toggle to optionally append error to the error toast * feat(frontend): Add default onError errorOverlay + add toggle to optionally append error to the error toast * feat(frontend): hide bug icon * feat(frontend): add missing ? * feat(frontend): simplify the code
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
export let toastId: string
|
||||
export let error: boolean = false
|
||||
export let actions: ToastAction[] = []
|
||||
export let errorMessage: string | undefined = undefined
|
||||
|
||||
function handleClose() {
|
||||
toast.pop(toastId)
|
||||
@@ -36,7 +37,13 @@
|
||||
</div>
|
||||
<div class="ml-3 flex-1 w-0">
|
||||
<p class="text-sm text-gray-500">{message}</p>
|
||||
{#if errorMessage}
|
||||
<p class="text-sm text-gray-500 border bg-gray-50 p-2 w-full overflow-auto mt-2">
|
||||
{errorMessage}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
export let customCss: ComponentCustomCSS<'buttoncomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let initializing: boolean | undefined = false
|
||||
export let errorHandledByComponent: boolean | undefined = false
|
||||
export let extraKey: string | undefined = undefined
|
||||
|
||||
export let controls: { left: () => boolean; right: () => boolean | string } | undefined =
|
||||
@@ -37,6 +38,7 @@
|
||||
)
|
||||
|
||||
$: initializing = resolvedConfig?.label == undefined
|
||||
$: errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
|
||||
|
||||
let outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
@@ -111,6 +113,8 @@
|
||||
bind:loading
|
||||
{componentInput}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
{errorHandledByComponent}
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
autoRefresh={false}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'formcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let errorHandledByComponent: boolean | undefined = false
|
||||
|
||||
$: errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
@@ -65,6 +68,8 @@
|
||||
{componentInput}
|
||||
{id}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
{errorHandledByComponent}
|
||||
{extraQueryParams}
|
||||
autoRefresh={false}
|
||||
forceSchemaDisplay={true}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'formbuttoncomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let errorHandledByComponent: boolean | undefined = false
|
||||
|
||||
$: errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
|
||||
|
||||
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -91,6 +94,8 @@
|
||||
runnableClass="!block"
|
||||
{outputs}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
{errorHandledByComponent}
|
||||
triggerable
|
||||
>
|
||||
<div class="flex flex-col gap-2 px-4 w-full">
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
result = res
|
||||
if (res?.error) {
|
||||
recordError(res.error)
|
||||
dispatch('handleError', res.error.message)
|
||||
} else {
|
||||
dispatch('success')
|
||||
}
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
|
||||
export let componentInput: AppInput | undefined
|
||||
|
||||
type SideEffectAction =
|
||||
| {
|
||||
selected: 'gotoUrl' | 'none' | 'setTab' | 'sendToast' | 'sendErrorToast' | 'errorOverlay'
|
||||
configuration: {
|
||||
gotoUrl: { url: string | undefined; newTab: boolean | undefined }
|
||||
setTab: {
|
||||
setTab: { id: string; index: number }[] | undefined
|
||||
}
|
||||
sendToast?: {
|
||||
message: string | undefined
|
||||
}
|
||||
sendErrorToast?: {
|
||||
message: string | undefined
|
||||
appendError: boolean | undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
|
||||
export let id: string
|
||||
export let result: any = undefined
|
||||
export let initializing: boolean = true
|
||||
@@ -22,20 +41,8 @@
|
||||
export let forceSchemaDisplay: boolean = false
|
||||
export let runnableClass = ''
|
||||
export let runnableStyle = ''
|
||||
export let doOnSuccess:
|
||||
| {
|
||||
selected: 'gotoUrl' | 'none' | 'setTab' | 'sendToast'
|
||||
configuration: {
|
||||
gotoUrl: { url: string | undefined; newTab: boolean | undefined }
|
||||
setTab: {
|
||||
setTab: { id: string; index: number }[] | undefined
|
||||
}
|
||||
sendToast: {
|
||||
message: string | undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
| undefined = undefined
|
||||
export let doOnSuccess: SideEffectAction = undefined
|
||||
export let doOnError: SideEffectAction = undefined
|
||||
|
||||
export let render: boolean
|
||||
export let recomputeIds: string[] = []
|
||||
@@ -75,17 +82,19 @@
|
||||
)
|
||||
}
|
||||
|
||||
export function onSuccess() {
|
||||
if (recomputeIds) {
|
||||
export function handleSideEffect(success: boolean, errorMessage?: string) {
|
||||
const sideEffect = success ? doOnSuccess : doOnError
|
||||
|
||||
if (recomputeIds && success) {
|
||||
recomputeIds.forEach((id) => $runnableComponents?.[id]?.cb())
|
||||
}
|
||||
if (!doOnSuccess) return
|
||||
if (!sideEffect) return
|
||||
|
||||
if (doOnSuccess.selected == 'none') return
|
||||
if (sideEffect.selected == 'none') return
|
||||
|
||||
if (doOnSuccess.selected == 'setTab') {
|
||||
if (Array.isArray(doOnSuccess.configuration.setTab.setTab)) {
|
||||
doOnSuccess.configuration.setTab?.setTab?.forEach((tab) => {
|
||||
if (sideEffect.selected == 'setTab') {
|
||||
if (Array.isArray(sideEffect.configuration.setTab.setTab)) {
|
||||
sideEffect.configuration.setTab?.setTab?.forEach((tab) => {
|
||||
if (tab) {
|
||||
const { id, index } = tab
|
||||
$componentControl[id].setTab?.(index)
|
||||
@@ -93,21 +102,34 @@
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
doOnSuccess.selected == 'gotoUrl' &&
|
||||
doOnSuccess.configuration.gotoUrl.url &&
|
||||
doOnSuccess.configuration.gotoUrl.url != ''
|
||||
sideEffect.selected == 'gotoUrl' &&
|
||||
sideEffect.configuration.gotoUrl.url &&
|
||||
sideEffect.configuration.gotoUrl.url != ''
|
||||
) {
|
||||
if (doOnSuccess.configuration.gotoUrl.newTab) {
|
||||
window.open(doOnSuccess.configuration.gotoUrl.url, '_blank')
|
||||
if (sideEffect.configuration.gotoUrl.newTab) {
|
||||
window.open(sideEffect.configuration.gotoUrl.url, '_blank')
|
||||
} else {
|
||||
goto(doOnSuccess.configuration.gotoUrl.url)
|
||||
goto(sideEffect.configuration.gotoUrl.url)
|
||||
}
|
||||
} else if (
|
||||
doOnSuccess.selected == 'sendToast' &&
|
||||
doOnSuccess.configuration.sendToast.message &&
|
||||
doOnSuccess.configuration.sendToast.message != ''
|
||||
sideEffect.selected == 'sendToast' &&
|
||||
sideEffect.configuration.sendToast &&
|
||||
sideEffect.configuration.sendToast.message &&
|
||||
sideEffect.configuration.sendToast.message != ''
|
||||
) {
|
||||
sendUserToast(doOnSuccess.configuration.sendToast.message)
|
||||
sendUserToast(sideEffect.configuration.sendToast.message, !success)
|
||||
} else if (
|
||||
sideEffect.selected == 'sendErrorToast' &&
|
||||
sideEffect.configuration.sendErrorToast &&
|
||||
sideEffect.configuration.sendErrorToast.message &&
|
||||
sideEffect.configuration.sendErrorToast.message != ''
|
||||
) {
|
||||
sendUserToast(
|
||||
sideEffect.configuration.sendErrorToast.message,
|
||||
true,
|
||||
[],
|
||||
sideEffect.configuration.sendErrorToast.appendError ? errorMessage : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -136,7 +158,8 @@
|
||||
wrapperClass={runnableClass}
|
||||
wrapperStyle={runnableStyle}
|
||||
{render}
|
||||
on:success={onSuccess}
|
||||
on:success={() => handleSideEffect(true)}
|
||||
on:handleError={(e) => handleSideEffect(false, e.detail)}
|
||||
{outputs}
|
||||
{errorHandledByComponent}
|
||||
>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let shouldHideActions: boolean = false
|
||||
export let hasInlineEditor: boolean = false
|
||||
export let inlineEditorOpened: boolean = false
|
||||
export let errorHandledByComponent: boolean = false
|
||||
|
||||
let isConditionalWrapperManuallySelected: boolean = false
|
||||
|
||||
@@ -165,7 +166,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
{#if error && !errorHandledByComponent}
|
||||
{@const json = JSON.parse(JSON.stringify(error))}
|
||||
<span
|
||||
title="Error"
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
movingcomponents != undefined && $mode == 'dnd' && $movingcomponents?.includes(component.id)
|
||||
|
||||
let initializing: boolean | undefined = undefined
|
||||
let errorHandledByComponent: boolean = false
|
||||
let componentContainerHeight: number = 0
|
||||
|
||||
let inlineEditorOpened: boolean = false
|
||||
@@ -94,6 +95,7 @@
|
||||
on:triggerInlineEditor={() => {
|
||||
inlineEditorOpened = !inlineEditorOpened
|
||||
}}
|
||||
{errorHandledByComponent}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -234,6 +236,7 @@
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:initializing
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
@@ -262,6 +265,7 @@
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'formbuttoncomponent'}
|
||||
@@ -273,6 +277,7 @@
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'checkboxcomponent'}
|
||||
|
||||
@@ -236,16 +236,20 @@ export const selectOptions = {
|
||||
formorientationOptions: ['Horizontal', 'Vertical']
|
||||
}
|
||||
|
||||
const labels = {
|
||||
none: 'Do nothing',
|
||||
errorOverlay: 'Show error overlay',
|
||||
gotoUrl: 'Go to an url',
|
||||
setTab: 'Set the tab of a tabs component',
|
||||
sendToast: 'Display a toast notification',
|
||||
sendErrorToast: 'Display an error toast notification'
|
||||
}
|
||||
|
||||
const onSuccessClick = {
|
||||
type: 'oneOf',
|
||||
tooltip: 'Action to perform on success',
|
||||
selected: 'none',
|
||||
labels: {
|
||||
none: 'Do nothing',
|
||||
gotoUrl: 'Go to an url',
|
||||
setTab: 'Set the tab of a tabs component',
|
||||
sendToast: 'Display a toast notification'
|
||||
},
|
||||
labels,
|
||||
configuration: {
|
||||
none: {},
|
||||
gotoUrl: {
|
||||
@@ -284,6 +288,55 @@ const onSuccessClick = {
|
||||
}
|
||||
} as const
|
||||
|
||||
const onErrorClick = {
|
||||
type: 'oneOf',
|
||||
tooltip: 'Action to perform on error',
|
||||
selected: 'errorOverlay',
|
||||
labels,
|
||||
configuration: {
|
||||
errorOverlay: {},
|
||||
gotoUrl: {
|
||||
url: {
|
||||
tooltip: 'Go to the given url, absolute or relative',
|
||||
fieldType: 'text',
|
||||
type: 'static',
|
||||
value: '',
|
||||
placeholder: '/apps/get/foo'
|
||||
},
|
||||
newTab: {
|
||||
tooltip: 'Open the url in a new tab',
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
value: true
|
||||
}
|
||||
},
|
||||
setTab: {
|
||||
setTab: {
|
||||
type: 'static',
|
||||
value: [] as Array<{ id: string; index: number }>,
|
||||
fieldType: 'array',
|
||||
subFieldType: 'tab-select',
|
||||
tooltip: 'Set the tabs id and index to go to on error'
|
||||
}
|
||||
},
|
||||
sendErrorToast: {
|
||||
message: {
|
||||
tooltip: 'The message of the toast to diplay',
|
||||
fieldType: 'text',
|
||||
type: 'static',
|
||||
value: '',
|
||||
placeholder: 'Hello there'
|
||||
},
|
||||
appendError: {
|
||||
tooltip: 'Append the error message to the toast',
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
const paginationOneOf = {
|
||||
type: 'oneOf',
|
||||
selected: 'auto',
|
||||
@@ -451,7 +504,8 @@ export const components = {
|
||||
fieldType: 'boolean',
|
||||
onlyStatic: true
|
||||
},
|
||||
onSuccess: onSuccessClick
|
||||
onSuccess: onSuccessClick,
|
||||
onError: onErrorClick
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -492,7 +546,8 @@ export const components = {
|
||||
onlyStatic: true,
|
||||
selectOptions: selectOptions.buttonSizeOptions
|
||||
},
|
||||
onSuccess: onSuccessClick
|
||||
onSuccess: onSuccessClick,
|
||||
onError: onErrorClick
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -535,6 +590,7 @@ export const components = {
|
||||
selectOptions: selectOptions.buttonSizeOptions
|
||||
},
|
||||
onSuccess: onSuccessClick,
|
||||
onError: onErrorClick,
|
||||
disabled: {
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
|
||||
@@ -87,7 +87,8 @@ export type ToastAction = {
|
||||
export function sendUserToast(
|
||||
message: string,
|
||||
error: boolean = false,
|
||||
actions: ToastAction[] = []
|
||||
actions: ToastAction[] = [],
|
||||
errorMessage: string | undefined = undefined
|
||||
): void {
|
||||
toast.push({
|
||||
component: {
|
||||
@@ -95,7 +96,8 @@ export function sendUserToast(
|
||||
props: {
|
||||
message,
|
||||
error,
|
||||
actions
|
||||
actions,
|
||||
errorMessage
|
||||
},
|
||||
sendIdTo: 'toastId'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user