Skip to content
Draft
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
103 changes: 89 additions & 14 deletions src/lib/components/payload/payload-code-block.svelte
Original file line number Diff line number Diff line change
@@ -1,34 +1,109 @@
<script lang="ts">
import Button from '$lib/holocene/button.svelte';
import CodeBlock from '$lib/holocene/code-block.svelte';
import Icon from '$lib/holocene/icon/icon.svelte';
import { translate } from '$lib/i18n/translate';
import type {
PayloadContainingObject,
PotentiallyDecodable,
import {
downloadExternalPayloadWithCodec,
NO_CODEC_SERVER_CONFIGURED_ERROR,
} from '$lib/services/data-encoder';
import { toaster } from '$lib/stores/toaster';
import type { Payload, Payloads } from '$lib/types';
import {
isExternallyStoredRawPayload,
isRawPayload,
parseRawPayloadToJSON,
type PayloadContainingObject,
} from '$lib/utilities/decode-payload';
import { formatBytes } from '$lib/utilities/format-bytes';
import { isNetworkError } from '$lib/utilities/is-network-error';
import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int';

import PayloadDecoder from './payload-decoder.svelte';

interface Props {
value: PotentiallyDecodable | PayloadContainingObject;
value: Payload | Payloads | PayloadContainingObject;
maxHeight?: number;
testId?: string;
}

let { value, maxHeight, testId }: Props = $props();

let downloadError: string | undefined = $state(undefined);

const downloadExternalPayload = async (payload: Payload) => {
let data: Payloads | undefined = undefined;
try {
data = await downloadExternalPayloadWithCodec(payload);
const parsed = parseRawPayloadToJSON(data.payloads[0], false);
const content = stringifyWithBigInt(parsed);
const a = document.createElement('a');
const file = new Blob([content], { type: 'json/plain' });
a.href = URL.createObjectURL(file);
a.download = 'payload.json';
a.click();
} catch (error) {
console.error(error);
if (isNetworkError(error) && error.statusCode === 404) {
downloadError = 'Unable to download payload file.';
toaster.push({
variant: 'error',
duration: 5000,
message:
'Unable to download file due to codec server not having a /download endpoint configured. Update codec server and try again.',
});
} else if (error === NO_CODEC_SERVER_CONFIGURED_ERROR) {
downloadError =
'Unable to download payload file. No codec server is configured.';
toaster.push({
variant: 'error',
duration: 5000,
message:
'Unable to download file due to no codec server being configured. Add a codec server with a /download endpoint and try again.',
});
}
}
};
</script>

<PayloadDecoder {value}>
{#snippet children(decodedValue)}
{#snippet children(results)}
<div class="space-y-2">
{#each decodedValue as data (data)}
<CodeBlock
content={data}
{maxHeight}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
{testId}
language="json"
/>
{#each results as result (result.decodedValue)}
{#if isExternallyStoredRawPayload(result.decodedValue)}
<CodeBlock
content={stringifyWithBigInt(result.decodedValue.data)}
{maxHeight}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
{testId}
language="json"
>
{#snippet headerActions()}
<Button
size="sm"
variant="ghost"
leadingIcon="download"
on:click={() => downloadExternalPayload(result.originalValue)}
>
{formatBytes(
result.decodedValue.externalPayloads?.[0].sizeBytes,
)}
</Button>
{/snippet}
</CodeBlock>
{#if downloadError}
<div class="flex items-center gap-1 text-danger">
<Icon name="exclamation-octagon" />
<p>{downloadError}</p>
</div>
{/if}
<p>
Payload downloads require a codec server with a <span
class="rounded-sm bg-code-block px-1 font-mono">/download</span
> endpoint.
</p>
{/if}
{/each}
</div>
{/snippet}
Expand Down
112 changes: 81 additions & 31 deletions src/lib/components/payload/payload-decoder.svelte
Original file line number Diff line number Diff line change
@@ -1,52 +1,102 @@
<script lang="ts" module>
export type DecodedPayloadResult = {
decodedValue: ParsedPayload | PayloadContainingObject;
originalValue: Payload | PayloadContainingObject;
}[];
</script>

<script lang="ts">
import { type Snippet } from 'svelte';

import type { Payload, Payloads } from '$lib/types';
import {
decodeEventAttributes,
decodePayloadAndParseDataToJSON,
decodePayloadsAndParseDataToJSON,
isRawPayload,
isRawPayloads,
type ParsedPayload,
type PayloadContainingObject,
type PotentiallyDecodable,
} from '$lib/utilities/decode-payload';
import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int';

export const decodePayloadValue = async (
value: PotentiallyDecodable | PayloadContainingObject,
): Promise<string[]> => {
type T = $$Generic<PayloadContainingObject>;

const decodePayloadValue = async (
value: Payload,
): Promise<DecodedPayloadResult> => {
const decodedPayload = await decodePayloadAndParseDataToJSON(value, false);
const result = [
{
decodedValue: decodedPayload,
originalValue: value,
},
];

onDecode?.(result);
return result;
};

const decodePayloadsValue = async (
value: Payloads,
): Promise<DecodedPayloadResult> => {
const decodedPayloads = await decodePayloadsAndParseDataToJSON(
value,
false,
);
const result = decodedPayloads.map((decodedPayload, idx) => {
return {
decodedValue: decodedPayload,
originalValue: value.payloads[idx],
};
});

onDecode?.(result);
return result;
};

const decodePayloadContainingObjectValue = async <
T extends PayloadContainingObject,
>(
value: T,
): Promise<DecodedPayloadResult> => {
const decodedValue = await decodeEventAttributes(value);
const result = [
{
decodedValue,
originalValue: value,
},
];

onDecode?.(result);
return result;
};

const decodeValue = (
value: Payload | Payloads | T,
): Promise<DecodedPayloadResult> => {
if (isRawPayload(value)) {
const decodedPayloadData = await decodePayloadAndParseDataToJSON(value);
const stringified = stringifyWithBigInt(decodedPayloadData);
onDecode?.([stringified]);
return [stringified];
} else if (isRawPayloads(value)) {
const parsedPayloadsData = await decodePayloadsAndParseDataToJSON(value);
const stringified = parsedPayloadsData.map((data) =>
stringifyWithBigInt(data),
);
onDecode?.(stringified);
return stringified;
} else {
const decoded = await decodeEventAttributes(value);
const stringified = stringifyWithBigInt(decoded);
onDecode?.([stringified]);
return [stringified];
return decodePayloadValue(value);
}

if (isRawPayloads(value)) {
return decodePayloadsValue(value);
}

return decodePayloadContainingObjectValue(value);
};

interface Props {
value: PotentiallyDecodable | PayloadContainingObject;
onDecode?: (decodedPayloads: string[]) => void;
children: Snippet<[decodedPayloads: string[]]>;
loading?: Snippet;
}
type Props = {
value: Payload | Payloads | T;
children: Snippet<[DecodedPayloadResult]>;
onDecode?: (result: DecodedPayloadResult) => void;
loading?: Snippet<[]>;
};

let { value, onDecode, children, loading }: Props = $props();
let { value, children, onDecode, loading }: Props = $props();
</script>

{#await decodePayloadValue(value)}
{#await decodeValue(value)}
{@render loading?.()}
{:then decodedValue}
{@render children(decodedValue)}
{:then decodeResult}
{@render children(decodeResult)}
{/await}
6 changes: 4 additions & 2 deletions src/lib/components/payload/payload-inline.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { PotentiallyDecodable } from '$lib/utilities/decode-payload';
import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int';

import PayloadDecoder from './payload-decoder.svelte';

Expand All @@ -13,12 +14,13 @@
</script>

<PayloadDecoder {value}>
{#snippet children(decodedValue)}
{#snippet children(result)}
{@const stringifiedData = stringifyWithBigInt(result[0].decodedValue.data)}
<div
class="overflow-hidden border border-subtle bg-code-block px-1 py-0.5 font-mono text-xs text-primary {className}"
>
<code>
<pre class="truncate">{decodedValue.slice(0, truncateAt)}</pre>
<pre class="truncate">{stringifiedData.slice(0, truncateAt)}</pre>
</code>
</div>
{/snippet}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import type { Writable } from 'svelte/store';

import PayloadDecoder from '$lib/components/payload/payload-decoder.svelte';
import PayloadDecoder, {
type DecodedPayloadResult,
} from '$lib/components/payload/payload-decoder.svelte';
import PayloadInputWithEncoding from '$lib/components/payload-input-with-encoding.svelte';
import Button from '$lib/holocene/button.svelte';
import { translate } from '$lib/i18n/translate';
Expand Down Expand Up @@ -35,8 +37,9 @@
let initialMessageType = $state('');
let loading = $state(true);

const setInitialInput = (decodedValue: string[]): void => {
initialInput = decodedValue[0];
const setInitialInput = (result: DecodedPayloadResult): void => {
initialInput = result[0].decodedValue.data;

input = initialInput;
let currentEncoding: PayloadInputEncoding = 'json/plain';
let currentMessageType = '';
Expand Down
4 changes: 3 additions & 1 deletion src/lib/holocene/code-block.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@
></div>

{#snippet actions()}
{#if copyable && !hasHeader}
{#if headerActions}
{@render headerActions()}
{:else if copyable && !hasHeader}
<CopyButton
{copyIconTitle}
{copySuccessIconTitle}
Expand Down
1 change: 1 addition & 0 deletions src/lib/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const Strings = {
'not-equal-to': 'Not equal to',
'encode-failed': 'Data encoding failed',
'decode-failed': 'Data decoding failed',
'download-failed': 'Download failed',
'job-id': 'Job ID',
'auto-refresh': 'Auto refresh',
'auto-refresh-tooltip': '{{ interval }} second page refresh',
Expand Down
31 changes: 23 additions & 8 deletions src/lib/services/data-encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
setLastDataEncoderFailure,
setLastDataEncoderSuccess,
} from '$lib/stores/data-encoder-config';
import type { Payloads } from '$lib/types';
import type { Payload, Payloads } from '$lib/types';
import type { NetworkError } from '$lib/types/global';
import { getAccessToken, getIdToken } from '$lib/utilities/core-provider';
import {
Expand All @@ -18,11 +18,15 @@ import { stringifyWithBigInt } from '$lib/utilities/parse-with-big-int';

export type PotentialPayloads = { payloads: unknown[] };

export const NO_CODEC_SERVER_CONFIGURED_ERROR = new Error(
'No codec server configured',
);

export async function codeServerRequest({
type,
payloads,
}: {
type: 'decode' | 'encode';
type: 'decode' | 'encode' | 'download';
payloads: PotentialPayloads;
}): Promise<Payloads> {
const settings = page.data.settings;
Expand All @@ -31,7 +35,7 @@ export async function codeServerRequest({

if (!endpoint) {
if (type === 'decode') return payloads;
throw new Error('No codec endpoint configured');
throw NO_CODEC_SERVER_CONFIGURED_ERROR;
}

const passAccessToken = getCodecPassAccessToken(settings);
Expand Down Expand Up @@ -71,10 +75,10 @@ export async function codeServerRequest({
body: stringifyWithBigInt(payloads),
};

const decoderResponse: Promise<PotentialPayloads> = fetch(
endpoint + `/${type}`,
requestOptions,
)
const url = new URL(type, endpoint);
url.searchParams.set('preserveStorageRefs', 'true');

const decoderResponse: Promise<PotentialPayloads> = fetch(url, requestOptions)
.then((response) => {
if (response.ok === false) {
throw {
Expand All @@ -93,7 +97,9 @@ export async function codeServerRequest({
return response;
})
.catch((err: unknown) => {
setLastDataEncoderFailure(err);
if (type !== 'download') {
setLastDataEncoderFailure(err);
}
if (type === 'decode') {
return payloads;
} else {
Expand All @@ -119,3 +125,12 @@ export async function encodePayloadsWithCodec({
}): Promise<Payloads> {
return codeServerRequest({ type: 'encode', payloads });
}

export async function downloadExternalPayloadWithCodec(
payload: Payload,
): Promise<Payloads> {
return codeServerRequest({
type: 'download',
payloads: { payloads: [payload] },
});
}
Loading