Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/components/EvidenceList.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<table class="table-fixed w-full rounded-full dark:text-slate-300">
<colgroup>
<col class="w-14" />
<col class="w-20" />
<col class="w-[30%]" />
<col class="w-48" />
<col v-if="configStore.showLabels" />
Expand Down Expand Up @@ -74,7 +74,7 @@
:key="item.uuid"
@click="openEvidence(item)"
>
<td class="py-2 pl-4 pr-2 w-[1%]">
<td class="py-2 pl-4 pr-6 w-[1%]">
<ResultStatusRing
class="p-0 m-0 whitespace-normal"
:state="item.status.state?.toLowerCase()"
Expand Down
119 changes: 117 additions & 2 deletions src/views/evidence/ViewView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,51 @@
:key="media.uuid"
class="border border-ccf-300 rounded-md overflow-hidden"
>
<BackMatterDisplay :resource="media" />
<div v-if="isRenderableTextMedia(media)">
<div class="flex items-stretch bg-white dark:bg-slate-950">
<button
type="button"
class="flex min-w-0 flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-medium text-gray-900 hover:bg-zinc-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-ccf-500 dark:text-slate-100 dark:hover:bg-slate-900"
:aria-expanded="isTextMediaExpanded(media.uuid)"
:aria-controls="textMediaContentId(media.uuid)"
:data-testid="`media-toggle-${media.uuid}`"
@click="toggleTextMedia(media.uuid)"
>
<span class="min-w-0 break-words">
{{ media.title || media.uuid }}
</span>
<span class="shrink-0 text-xs" aria-hidden="true">
{{ isTextMediaExpanded(media.uuid) ? '-' : '+' }}
</span>
</button>
<a
v-if="hasBase64MediaPayload(media)"
:download="media.title || media.uuid"
:href="mediaDataHref(media)"
:aria-label="`Download ${media.title || media.uuid}`"
class="flex shrink-0 items-center px-4 text-gray-700 hover:bg-zinc-50 hover:text-ccf-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-ccf-500 dark:text-slate-200 dark:hover:bg-slate-900"
>
<BIconDownload />
</a>
</div>
<pre
v-if="isTextMediaExpanded(media.uuid)"
:id="textMediaContentId(media.uuid)"
class="max-h-[32rem] overflow-auto p-4 text-sm leading-6 text-gray-900 whitespace-pre-wrap break-words dark:text-slate-100"
>{{ renderMediaText(media) }}</pre
>
</div>
<BackMatterDisplay v-else :resource="media" />
Comment thread
gusfcarvalho marked this conversation as resolved.
<div
v-if="!isRenderableTextMedia(media)"
class="border-t border-ccf-300 py-2 px-4 flex justify-between items-center"
>
<span>{{ media.title || media.uuid }}</span>
<a
v-if="hasBase64MediaPayload(media)"
:download="media.title || media.uuid"
:href="`data:${media.base64?.mediaType};base64,${media.base64?.value}`"
:href="mediaDataHref(media)"
:aria-label="`Download ${media.title || media.uuid}`"
>
<BIconDownload />
</a>
Expand Down Expand Up @@ -747,6 +784,7 @@ const activeTab = ref<(typeof tabs)[number]['id']>('overview');
const activities = ref<Activity[]>([] as Activity[]);
const showActivitiesModal = ref(false);
const verificationAttempted = ref(false);
const expandedTextMedia = ref<Set<string>>(new Set());

const {
data: evidence,
Expand Down Expand Up @@ -1014,13 +1052,90 @@ function getSafeExternalHref(value?: string) {
return '';
}

function getMediaType(resource: BackMatterResource): string {
return resource.base64?.mediaType?.split(';')[0]?.trim().toLowerCase() ?? '';
}

function hasBase64MediaPayload(resource: BackMatterResource): boolean {
return Boolean(resource.base64?.mediaType && resource.base64?.value);
}

function mediaDataHref(resource: BackMatterResource): string {
return `data:${resource.base64?.mediaType};base64,${resource.base64?.value}`;
}

function isRenderableTextMedia(resource: BackMatterResource): boolean {
if (!resource.base64?.value) {
return false;
}

const mediaType = getMediaType(resource);

return (
mediaType.startsWith('text/') ||
mediaType === 'application/json' ||
mediaType.endsWith('+json') ||
mediaType === 'application/yaml' ||
mediaType === 'application/x-yaml' ||
mediaType.endsWith('+yaml')
);
}

function isTextMediaExpanded(uuid: string): boolean {
return expandedTextMedia.value.has(uuid);
}

function textMediaContentId(uuid: string): string {
return `evidence-media-content-${uuid}`;
}

function toggleTextMedia(uuid: string) {
const nextExpandedTextMedia = new Set(expandedTextMedia.value);

if (nextExpandedTextMedia.has(uuid)) {
nextExpandedTextMedia.delete(uuid);
} else {
nextExpandedTextMedia.add(uuid);
}

expandedTextMedia.value = nextExpandedTextMedia;
}

function decodeBase64Text(value: string): string {
try {
const bytes = Uint8Array.from(atob(value), (character) =>
character.charCodeAt(0),
);

return new TextDecoder().decode(bytes);
} catch {
return value;
}
}

function renderMediaText(resource: BackMatterResource): string {
const text = decodeBase64Text(resource.base64?.value ?? '');
const mediaType = getMediaType(resource);

if (mediaType === 'application/json' || mediaType.endsWith('+json')) {
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch {
return text;
}
}

return text;
}

watch(
evidenceId,
async (id) => {
verificationAttempted.value = false;
verificationResult.value = undefined;
verifyError.value = undefined;
showActivitiesModal.value = false;
expandedTextMedia.value = new Set();
evidence.value = undefined;
signatureDetail.value = undefined;
latestEvidence.value = undefined;
Expand Down
72 changes: 72 additions & 0 deletions src/views/evidence/__tests__/ViewView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const {
expires: '2099-04-10T10:15:00Z',
links: [
{ href: '#resource-1', rel: 'reference', text: 'Attestation bundle' },
{ href: '#resource-3', rel: 'reference', text: 'JSON evidence' },
{ href: '#resource-4', rel: 'reference', text: 'YAML evidence' },
{
href: 'https://example.com/evidence',
rel: 'external',
Expand Down Expand Up @@ -101,6 +103,22 @@ const {
title: 'Unlinked resource',
base64: { mediaType: 'text/plain', value: 'YmFy' },
},
{
uuid: 'resource-3',
title: 'JSON resource',
base64: {
mediaType: 'application/json',
value: 'eyJzdGF0dXMiOiJwYXNzIn0=',
},
},
{
uuid: 'resource-4',
title: 'YAML resource',
base64: {
mediaType: 'application/yaml',
value: 'c3RhdHVzOiBwYXNzCg==',
},
},
],
},
status: {
Expand Down Expand Up @@ -579,9 +597,63 @@ describe('Evidence ViewView', () => {
await clickButtonByText(wrapper, 'Media');

expect(wrapper.text()).toContain('Attestation bundle');
expect(wrapper.text()).toContain('JSON resource');
expect(wrapper.text()).toContain('YAML resource');
expect(wrapper.text()).not.toContain('Unlinked resource');
});

it('renders text, JSON, and YAML media after the user expands each resource', async () => {
const wrapper = mountView();
await flushPromises();

await clickButtonByText(wrapper, 'Media');

expect(wrapper.text()).not.toContain('foo');
expect(wrapper.text()).not.toContain('"status": "pass"');
expect(wrapper.text()).not.toContain('status: pass');
expect(wrapper.findAll('a[download]')).toHaveLength(3);

await wrapper
.find('[data-testid="media-toggle-resource-1"]')
.trigger('click');
await wrapper
.find('[data-testid="media-toggle-resource-3"]')
.trigger('click');
await wrapper
.find('[data-testid="media-toggle-resource-4"]')
.trigger('click');

expect(wrapper.text()).toContain('foo');
expect(wrapper.text()).toContain('"status": "pass"');
expect(wrapper.text()).toContain('status: pass');
});

it('collapses rendered text media after a second title click', async () => {
const wrapper = mountView();
await flushPromises();

await clickButtonByText(wrapper, 'Media');

const toggle = wrapper.find('[data-testid="media-toggle-resource-1"]');
expect(toggle.attributes('aria-expanded')).toBe('false');
expect(toggle.attributes('aria-controls')).toBe(
'evidence-media-content-resource-1',
);

await toggle.trigger('click');

expect(toggle.attributes('aria-expanded')).toBe('true');
expect(wrapper.find('#evidence-media-content-resource-1').exists()).toBe(
true,
);
expect(wrapper.text()).toContain('foo');

await toggle.trigger('click');

expect(toggle.attributes('aria-expanded')).toBe('false');
expect(wrapper.text()).not.toContain('foo');
});

it('shows signed metadata and only verifies on manual action', async () => {
const wrapper = mountView();
await flushPromises();
Expand Down
Loading