Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/components/MessageItem.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { computed, defineComponent, PropType, toRefs } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message, MessageType } from '../store/messages';

const MessageTypeClass: Record<MessageType, string> = {
Expand All @@ -18,6 +19,7 @@ export default defineComponent({
},
setup(props) {
const { message } = toRefs(props);
const { copy, copied } = useClipboard();

const headerClass = computed(() => {
const type = MessageTypeClass[message.value.type];
Expand All @@ -27,8 +29,15 @@ export default defineComponent({
return '';
});

const copyBugReport = () => {
const report = message.value.bugReport;
if (report) copy(report);
};

return {
headerClass,
copied,
copyBugReport,
};
},
});
Expand All @@ -39,13 +48,25 @@ export default defineComponent({
<v-expansion-panel-title :class="headerClass">
<div class="header">
<span>{{ message.title }}</span>
<v-btn
icon="mdi-delete"
variant="text"
size="small"
class="mr-3"
@click.stop="$emit('delete')"
/>
<div>
<v-btn
v-if="message.bugReport"
:prepend-icon="copied ? 'mdi-check' : 'mdi-content-copy'"
variant="tonal"
size="small"
data-testid="copy-bug-report-button"
@click.stop="copyBugReport"
>
Copy Bug Report
</v-btn>
<v-btn
icon="mdi-delete"
variant="text"
size="small"
class="mr-3"
@click.stop="$emit('delete')"
/>
</div>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text v-if="message.options.details">
Expand Down
1 change: 1 addition & 0 deletions src/components/PatientStudyVolumeBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export default defineComponent({
if (err instanceof Error) {
const messageStore = useMessageStore();
messageStore.addError('Failed to generate thumbnails', {
error: err,
details: `${err}. More details can be found in the developer's console.`,
});
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/SampleDataBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export default defineComponent({
} catch (error) {
status.progress[sample.name].state = ProgressState.Error;
const messageStore = useMessageStore();
messageStore.addError('Failed to load sample data', error as Error);
messageStore.addError('Failed to load sample data', {
error: error as Error,
});
} finally {
delete status.progress[sample.name];
}
Expand Down
1 change: 1 addition & 0 deletions src/composables/useErrorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export async function useErrorMessage(message: string, task: Function) {
if (err instanceof Error) {
const messageStore = useMessageStore();
messageStore.addError(message, {
error: err,
details: `${err}. More details can be found in the developer's console.`,
});
}
Expand Down
12 changes: 8 additions & 4 deletions src/composables/useGlobalErrorHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ export function useGlobalErrorHook() {

const onError = (event: ErrorEvent) => {
console.error(event);
const errorMessage = event.message ?? 'Unknown global error';
const error = event.error ?? event.message ?? 'Unknown global error';

captureException(event.error ?? errorMessage);
captureException(error);

const details = event.error ? event.error : { details: errorMessage };
messageStore.addError('Application error (click for details)', details);
const errorOptions =
error instanceof Error ? { error } : { details: String(error) };
messageStore.addError(
'Application error (click for details)',
errorOptions
);
};

onMounted(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/composables/useWebGLWatchdog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ function getVolumeMapperContext(view: View) {
export function useWebGLWatchdog(view: MaybeRef<Maybe<View>>) {
const reportError = useThrottleFn(() => {
const messageStore = useMessageStore();
messageStore.addError(Messages.WebGLLost.title, Messages.WebGLLost.details);
messageStore.addError(Messages.WebGLLost.title, {
details: Messages.WebGLLost.details,
});

const contexts: Record<string, any> = {};
const viewVal = unref(view);
Expand Down
1 change: 1 addition & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ interface ImportMeta {
}

declare const __VERSIONS__: Record<string, string>;
declare const __GIT_SHORT_SHA__: string;
7 changes: 3 additions & 4 deletions src/io/import/processors/restoreStateFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ function resolveToLeafSources(
return [{ type: 'file', file, fileType: src.fileType }];
}
const missingFile = filePath ?? String(src.fileId);
useMessageStore().addError(
'State file missing expected file',
missingFile
);
useMessageStore().addError('State file missing expected file', {
details: missingFile,
});
return [];
}

Expand Down
6 changes: 3 additions & 3 deletions src/store/__tests__/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Message store', () => {

const innerError = new Error('inner error');
const ids = [
messageStore.addError('an error', innerError),
messageStore.addError('an error', { error: innerError }),
messageStore.addWarning('warning'),
messageStore.addInfo('info'),
messageStore.addInfo('loading', {
Expand All @@ -26,7 +26,7 @@ describe('Message store', () => {
type: MessageType.Error,
title: 'an error',
options: {
details: String(innerError),
details: innerError.stack ?? String(innerError),
persist: false,
},
},
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('Message store', () => {
expect(messageStore.messages).to.have.length(4);

ids.forEach((id, index) => {
expect(messageStore.byID[id]).to.deep.equal(expected[index]);
expect(messageStore.byID[id]).toMatchObject(expected[index]);
});

messageStore.clearOne(ids[1]);
Expand Down
2 changes: 1 addition & 1 deletion src/store/dicom-web/dicom-web-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export const useDicomWebStore = defineStore('dicom-web', () => {
};
} catch (error) {
const messageStore = useMessageStore();
messageStore.addError('Failed to load DICOM', error as Error);
messageStore.addError('Failed to load DICOM', { error: error as Error });
volumes.value[volumeKey] = {
...volumes.value[volumeKey],
state: 'Error',
Expand Down
2 changes: 1 addition & 1 deletion src/store/image-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const useImageCacheStore = defineStore('image-cache', () => {
imageErrors[id].push(error);

const messageStore = useMessageStore();
messageStore.addError('Error loading DICOM data', error);
messageStore.addError('Error loading DICOM data', { error });
};

imageListenerCleanup[id] = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/store/image-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const useImageStatsStore = defineStore('image-stats', () => {
);
messageStore.addError(
`Auto range computation failed for image ${id}`,
ensureError(error)
{ error: ensureError(error) }
);
})
.finally(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/store/load-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function useLoadingNotifications() {
if (error) {
logError(error);
toast.dismiss(toastID);
messageStore.addError(NotificationMessages.Error, error);
messageStore.addError(NotificationMessages.Error, { error });
} else {
toast.update(toastID, {
content: NotificationMessages.Done,
Expand Down
36 changes: 16 additions & 20 deletions src/store/messages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { removeFromArray } from '../utils';
import { generateBugReport } from '../utils/bugReport';

export enum MessageType {
Error,
Expand All @@ -18,10 +19,18 @@ export interface Message {
type: MessageType;
title: string;
options: MessageOptions;
bugReport?: string;
}

export type ErrorOptions = {
error?: Error;
details?: string;
persist?: boolean;
};

export type UpdateProgressFunction = (progress: number) => void;
export type TaskFunction = (updateProgress?: UpdateProgressFunction) => any;

interface State {
_nextID: number;
byID: Record<string, Message>;
Expand All @@ -44,32 +53,19 @@ export const useMessageStore = defineStore('message', {
},
},
actions: {
/**
* Adds an error message.
* @param title message title
* @param opts an Error, a string containing details, or a MessageOptions
*/
addError(title: string, opts?: Error | string | MessageOptions) {
console.error(title, opts);
addError(title: string, opts?: ErrorOptions) {
console.error(title, opts?.error ?? opts?.details);

if (opts instanceof Error) {
return this._addMessage(
{
type: MessageType.Error,
title,
},
{
details: String(opts),
persist: false,
}
);
}
return this._addMessage(
{
type: MessageType.Error,
title,
bugReport: generateBugReport(opts?.error),
},
opts
{
details: opts?.details ?? opts?.error?.stack,
persist: opts?.persist ?? false,
}
);
},
/**
Expand Down
9 changes: 7 additions & 2 deletions src/store/remote-save-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ const useRemoteSaveStateStore = defineStore('remoteSaveState', () => {
});

if (saveResult.ok) messageStore.addSuccess('Save Successful');
else messageStore.addError('Save Failed', 'Network response not OK');
else
messageStore.addError('Save Failed', {
details: 'Network response not OK',
});
} catch (error) {
messageStore.addError('Save Failed with error', `Failed from: ${error}`);
messageStore.addError('Save Failed with error', {
details: `Failed from: ${error}`,
});
} finally {
isSaving.value = false;
}
Expand Down
7 changes: 3 additions & 4 deletions src/store/tools/paintProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,9 @@ export const usePaintProcessStore = defineStore('paintProcess', () => {
showingOriginal: false,
};
} catch (error) {
messageStore.addError(
`${activeProcessType.value} Operation Failed`,
error as Error
);
messageStore.addError(`${activeProcessType.value} Operation Failed`, {
error: error as Error,
});
if (processState.value.step === 'computing') {
rollbackPreview(segImage, originalScalars);
resetState();
Expand Down
94 changes: 94 additions & 0 deletions src/utils/bugReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* global __VERSIONS__, __GIT_SHORT_SHA__ */

import { useDatasetStore } from '@/src/store/datasets';
import { useDICOMStore } from '@/src/store/datasets-dicom';
import { useImageCacheStore } from '@/src/store/image-cache';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';

const MAX_ERROR_LENGTH = 4000;

const COMPOUND_EXTENSIONS = ['nii.gz', 'iwi.cbor', 'seg.nrrd'];

const getBrowserInfo = (): string =>
typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown';

const getSourceFormat = (name: string, isDicom: boolean): string => {
if (isDicom) return 'DICOM';
const lower = name.toLowerCase();
const compound = COMPOUND_EXTENSIONS.find((ext) => lower.endsWith(`.${ext}`));
if (compound) return compound;
const lastDot = name.lastIndexOf('.');
return lastDot >= 0 ? name.slice(lastDot + 1).toLowerCase() : 'unknown';
};

const formatScalarType = (constructorName: string): string =>
constructorName.replace('Array', '');

const formatError = (error?: Error): string => {
if (!error) return 'No error details available';
const text = error.stack ?? String(error);
return text.length > MAX_ERROR_LENGTH
? `${text.slice(0, MAX_ERROR_LENGTH)}\n... (truncated)`
: text;
};

const collectDatasetInfo = (): string[] => {
const datasetStore = useDatasetStore();
const imageCacheStore = useImageCacheStore();
const dicomStore = useDICOMStore();
const segmentGroupStore = useSegmentGroupStore();

return datasetStore.idsAsSelections.map((id, i) => {
const metadata = imageCacheStore.getImageMetadata(id);
const imageData = imageCacheStore.getVtkImageData(id);

const dims = metadata
? Array.from(metadata.dimensions).join('\u00d7')
: 'unknown';

const scalars = imageData?.getPointData().getScalars()?.getData();
const dataType = scalars
? formatScalarType(scalars.constructor.name)
: 'unknown';

const isDicom = id in dicomStore.volumeInfo;
const sourceFormat = metadata
? getSourceFormat(metadata.name, isDicom)
: isDicom
? 'DICOM'
: 'unknown';

const segCount = segmentGroupStore.orderByParent[id]?.length ?? 0;
const segPart =
segCount > 0
? ` (segment groups: ${segCount} as ${segmentGroupStore.saveFormat})`
: '';

return ` [${i}] ${dims} ${dataType} from ${sourceFormat}${segPart}`;
});
};

export const generateBugReport = (error?: Error): string => {
const versions = __VERSIONS__;
const sha = __GIT_SHORT_SHA__;

const lines = [
'--- VolView Bug Report ---',
`Build: volview ${versions.volview} (${sha}) | vtk.js: ${versions['vtk.js']}, itk-wasm: ${versions['itk-wasm']}`,
`Browser: ${getBrowserInfo()}`,
'',
'Error:',
formatError(error),
];

const datasets = collectDatasetInfo();
const segmentGroupStore = useSegmentGroupStore();

lines.push('', `Datasets: ${datasets.length}`);
lines.push(...datasets);
lines.push(`Save format: ${segmentGroupStore.saveFormat}`);

lines.push('--- End Report ---');

return lines.join('\n');
};
Loading
Loading