Skip to content

Commit

Permalink
Add support for SSR, revamp error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
bcspragu committed Sep 13, 2023
1 parent 2525f7f commit a267f18
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 102 deletions.
25 changes: 25 additions & 0 deletions frontend/app.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
<script setup lang="ts">
const { loading: { onMountedWithLoading, clearLoading }, error: { errorModalVisible, error } } = useModal()
const handleError = (err: Error) => {
error.value = err
errorModalVisible.value = true
clearLoading()
}
onErrorCaptured((err: unknown, _instance: ComponentPublicInstance | null, _info: string) => {
let error: Error | undefined
if (err instanceof Error) {
error = err
} else if (typeof (err) === 'string') {
error = new Error(err)
} else {
error = new Error('unknown error', { cause: err })
}
handleError(error)
return false // Don't propagate
})
onMountedWithLoading(() => { /* nothing to do */ }, 'defaultLayout.onMountedWithLoading')
</script>

<template>
<NuxtLayout />
</template>
50 changes: 41 additions & 9 deletions frontend/components/modal/Error.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
<script setup lang="ts">
const { error: { errorModalVisible, error } } = useModal()
import { watch } from 'vue'
interface Props {
isFullPage?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isFullPage: false
})
const { error: { errorModalVisible, error: modalError } } = useModal()
const error = useError()
const router = useRouter()
watch(errorModalVisible, async (newV, oldV) => {
if (!props.isFullPage) {
return
}
// We only care about the case where the modal was just closed (i.e. has gone from visible -> not visible).
if (newV || !oldV) {
return
}
if (window.history.length > 1) {
await clearError().then(router.back)
} else {
await clearError({ redirect: '/' })
}
})
const fullError = computed(() => {
return error.value
? {
name: error.value.name,
message: error.value.message,
stack: error.value.stack?.split('\n')
}
: ''
const err = error.value ?? modalError.value
if (err instanceof Error) {
return {
name: err.name ?? '',
message: err.message,
stack: err.stack?.split('\n')
}
} else if (err) {
return err
} else {
return ''
}
})
</script>

Expand All @@ -19,7 +51,7 @@ const fullError = computed(() => {
sub-header="Sorry about that, our team take bug reports seriously, and will try to make it right!"
>
<StandardDebug
label="Error Trace"
label="Error Details"
:value="fullError"
always
/>
Expand Down
31 changes: 14 additions & 17 deletions frontend/components/standard/Debug.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,22 @@ const props = withDefaults(defineProps<Props>(), { always: false, label: 'Techni
</script>

<template>
<!-- TODO(#9) Remove this ClientOnly once the ULS is fixed. -->
<ClientOnly>
<PVAccordion
v-if="showStandardDebug || props.always"
class="standard-debug"
<PVAccordion
v-if="showStandardDebug || props.always"
class="standard-debug"
>
<PVAccordionTab
:header="props.label || 'Debug'"
content-class="surface-100"
header-class="surface-800"
>
<PVAccordionTab
:header="props.label || 'Debug'"
content-class="surface-100"
header-class="surface-800"
<div
class="code surface-50"
>
<div
class="code surface-50"
>
{{ JSON.stringify(props.value, null, 2) }}
</div>
</PVAccordionTab>
</PVAccordion>
</ClientOnly>
{{ JSON.stringify(props.value, null, 2) }}
</div>
</PVAccordionTab>
</PVAccordion>
</template>

<style lang="scss">
Expand Down
27 changes: 7 additions & 20 deletions frontend/composables/newModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,7 @@ export const useModal = () => {

// error
const errorModalVisible = newModalVisibilityState('errorModalVisible')
const error = useState<Error | null>(`${prefix}.error`, () => null)
const setError = (opKey: string) => {
return (err?: Error) => {
error.value = err ?? new Error('an unknown error occurred')
errorModalVisible.value = true
clearLoading()

// We used to re-throw here, but that just breaks the page (e.g. no more
// navigation), since it ends up propagating to the top-level. Since
// setError is 'handling' the error, we don't re-throw.
}
}
const withErrorHandling = async (fn: () => Promise<unknown>, opKey: string): Promise<unknown> => {
return await fn().catch(setError(opKey))
}
const error = useState<Error>('errorModal.error')

// loading
const loadingSet = useState<Set<string>>(`${prefix}.loadingSet`, () => new Set<string>())
Expand All @@ -52,7 +38,7 @@ export const useModal = () => {
const withLoadingAndErrorHandling = async <T> (fn: () => Promise<T>, opKey: string): (Promise<T>) => {
startLoading(opKey)
const p = fn()
p.catch(setError(opKey)).finally(stopLoading(opKey))
void p.finally(stopLoading(opKey))
return await p
}
const onMountedWithLoading = (fn: () => void, opKey: string) => {
Expand All @@ -74,10 +60,13 @@ export const useModal = () => {
const anyBlockingModalOpen = computed(() => anyModalVisible.value || loading.value)

const handleOAPIError = async <T>(t: OPAIError | T): Promise<T> => {
return await new Promise<T>((resolve, reject) => {
return await new Promise<T>((resolve) => {
// TODO(#10) Rephrase this once we use 300+ for all errors
if (t instanceof Object && Object.prototype.hasOwnProperty.call(t, 'message')) {
reject(new Error(JSON.stringify(t)))
throw createError({
message: 'error from API',
cause: t
})
} else {
resolve(t as T)
}
Expand All @@ -97,9 +86,7 @@ export const useModal = () => {
loadingSet
},
error: {
setError,
error,
withErrorHandling,
errorModalVisible,
withLoadingAndErrorHandling,
handleOAPIError
Expand Down
11 changes: 10 additions & 1 deletion frontend/composables/useAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export const useAPI = (): API => {
CREDENTIALS: 'include' as const, // To satisfy typing of 'include' | 'same-origin' | etc
WITH_CREDENTIALS: true
}

let headers: Record<string, string> = {}
if (process.server) {
headers = Object.entries(useRequestHeaders(['cookie']))
.filter((ent) => !!ent[1])
.reduce((a, v) => ({ ...a, [v[0]]: v[1] }), {})
}

const userCfg = {
...baseCfg,
BASE: authServerURL
Expand All @@ -21,7 +29,8 @@ export const useAPI = (): API => {

const pactaClient = new PACTAClient({
...baseCfg,
BASE: apiServerURL
BASE: apiServerURL,
HEADERS: headers
})

return {
Expand Down
13 changes: 13 additions & 0 deletions frontend/error.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
// We get the equivalent of props.error from useError() in the ModalError component.
// const props = defineProps({
// error: Object
// })
const { error: { errorModalVisible } } = useModal()
errorModalVisible.value = true
</script>

<template>
<ModalError :is-full-page="true" />
</template>
24 changes: 2 additions & 22 deletions frontend/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
<script setup lang="ts">
import { onMounted } from 'vue'
const { loading: { onMountedWithLoading, loadingSet }, anyBlockingModalOpen, error: { setError } } = useModal()
const handleError = (event: Event & { reason: Error }) => {
event.preventDefault()
const { reason } = event
setError('fallback')(reason)
loadingSet.value.clear()
}
onMountedWithLoading(() => { /* nothing to do */ }, 'defaultLayout.onMountedWithLoading')
onMounted(() => {
window.addEventListener('unhandledrejection', handleError)
})
const { anyBlockingModalOpen } = useModal()
</script>

<template>
Expand All @@ -28,13 +14,7 @@ onMounted(() => {
class="px-3 md:px-4 w-full lg:w-10 xl:w-8 mx-auto"
style="min-height: calc(100vh - 9rem - 4px);"
>
<NuxtErrorBoundary>
<template #error="{ error, clearError }">
{{ setError(error) }}
{{ clearError() }}
</template>
<NuxtPage />
</NuxtErrorBoundary>
<NuxtPage />
</main>
</div>
<ModalGroup />
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/admin/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ const adminItems: AdminItem[] = [
</PVCard>
</div>
</div>
</standardcontent>
</StandardContent>
</template>
40 changes: 23 additions & 17 deletions frontend/pages/admin/pacta-version/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,27 @@ const { fromParams } = useURLParams()
const id = presentOrCheckURL(fromParams('id'))
const prefix = 'admin/pacta-version/[id]'
const persistedPactaVersion = useState<PactaVersion>(`${prefix}.persistedPactaVersion`)
const prefix = `admin/pacta-version/${id}`
const pactaVersion = useState<PactaVersion>(`${prefix}.pactaVersion`)
const { data: persistedPactaVersion, error, refresh } = await useAsyncData(`${prefix}.getPactaVersion`, () => {
return withLoadingAndErrorHandling(() => {
return pactaClient.findPactaVersionById(id)
.then(handleOAPIError)
}, `${prefix}.getPactaVersion`)
})
if (error.value) {
throw createError(error.value)
}
if (!persistedPactaVersion.value) {
throw new Error('PACTA version not found')
}
pactaVersion.value = { ...persistedPactaVersion.value }
const refreshPACTA = async () => {
await refresh()
if (persistedPactaVersion.value) {
pactaVersion.value = { ...persistedPactaVersion.value }
}
}
const changes = computed<PactaVersionChanges>(() => {
const a = persistedPactaVersion.value
Expand All @@ -27,7 +45,7 @@ const hasChanges = computed<boolean>(() => Object.keys(changes.value).length > 0
const markDefault = () => withLoadingAndErrorHandling(
() => pactaClient.markPactaVersionAsDefault(id)
.then(handleOAPIError)
.then(() => { pactaVersion.value.isDefault = true }),
.then(refreshPACTA),
`${prefix}.markPactaVersionAsDefault`
)
const deletePV = () => withLoadingAndErrorHandling(
Expand All @@ -39,23 +57,10 @@ const deletePV = () => withLoadingAndErrorHandling(
const saveChanges = () => withLoadingAndErrorHandling(
() => pactaClient.updatePactaVersion(id, changes.value)
.then(handleOAPIError)
.then(() => { persistedPactaVersion.value = pactaVersion.value })
.then(refreshPACTA)
.then(() => router.push('/admin/pacta-version')),
`${prefix}.saveChanges`
)
// TODO(#13) Remove this from the on-mounted hook
onMounted(async () => {
await withLoadingAndErrorHandling(
() => pactaClient.findPactaVersionById(id)
.then(handleOAPIError)
.then(pv => {
pactaVersion.value = { ...pv }
persistedPactaVersion.value = { ...pv }
}),
`${prefix}.getPactaVersion`
)
})
</script>

<template>
Expand Down Expand Up @@ -85,6 +90,7 @@ onMounted(async () => {
label="Discard Changes"
icon="pi pi-arrow-left"
class="p-button-secondary p-button-outlined"
to="/admin/pacta-version"
/>
<PVButton
:disabled="!hasChanges"
Expand Down
22 changes: 7 additions & 15 deletions frontend/pages/admin/pacta-version/index.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
<script setup lang="ts">
import { type PactaVersion } from '@/openapi/generated/pacta'
const router = useRouter()
const { pactaClient } = useAPI()
const { error: { withLoadingAndErrorHandling, handleOAPIError } } = useModal()
const prefix = 'admin/pacta-version'
const pactaVersions = useState<PactaVersion[]>(`${prefix}.pactaVersions`, () => [])
const { data: pactaVersions, refresh } = await useAsyncData(`${prefix}.getPactaVersions`, () => {
return withLoadingAndErrorHandling(() => {
return pactaClient.listPactaVersions().then(handleOAPIError)
}, `${prefix}.getPactaVersions`)
})
const newPV = () => router.push('/admin/pacta-version/new')
const markDefault = (id: string) => withLoadingAndErrorHandling(
() => pactaClient.markPactaVersionAsDefault(id)
.then(handleOAPIError)
.then(() => { pactaVersions.value = pactaVersions.value.map(pv => ({ ...pv, isDefault: id === pv.id })) }),
.then(refresh),
`${prefix}.markPactaVersionAsDefault`
)
const deletePV = (id: string) => withLoadingAndErrorHandling(
() => pactaClient.deletePactaVersion(id)
.then(handleOAPIError)
.then(() => { pactaVersions.value = pactaVersions.value.filter(pv => pv.id !== id) }),
.then(refresh),
`${prefix}.deletePactaVersion`
)
// TODO(#13) Remove this from the on-mounted hook
onMounted(async () => {
await withLoadingAndErrorHandling(
() => pactaClient.listPactaVersions()
.then(handleOAPIError)
.then(pvs => { pactaVersions.value = pvs }),
`${prefix}.getPactaVersions`
)
})
</script>

<template>
Expand Down

0 comments on commit a267f18

Please sign in to comment.