Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
116 changes: 112 additions & 4 deletions src/lib/components/event/event-details-full.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<script lang="ts">
import { page } from '$app/state';

import type { EventGroup } from '$lib/models/event-groups/event-groups';
import type { WorkflowEvent } from '$lib/types/events';
import { formatAttributes } from '$lib/utilities/format-event-attributes';
import { displayLinkType } from '$lib/utilities/get-single-attribute-for-event';

import PendingActivityCard from '../workflow/pending-activity/pending-activity-card.svelte';
import PendingNexusOperationCard from '../workflow/pending-nexus-operation/pending-nexus-operation-card.svelte';

import EventCard from './event-card.svelte';
import EventDetailsSection from './event-details-section.svelte';
import EventPayloads from './event-payloads.svelte';

let {
group = undefined,
Expand All @@ -18,18 +24,120 @@
const showEventGroup = $derived(
group && (group.eventList.length > 1 || pendingEvent),
);

const processedEvents = $derived.by(() => {
if (!group || !showEventGroup) return [];
return group.eventList.map((evt) => {
const attrs = formatAttributes(evt);
if (evt?.principal?.name) attrs.principalName = evt.principal.name;
if (evt?.principal?.type) attrs.principalType = evt.principal.type;
const fields = Object.entries(attrs);
const payloadFields = fields.filter(
([_key, value]) =>
typeof value === 'object' && Object.keys(value).length > 0,
);
const linkFields = fields.filter(
([key, _value]) => displayLinkType(key, attrs) !== 'none',
);
const hiddenDetailFields = (() => {
if (evt.category === 'activity')
return ['scheduledEventId', 'startedEventId', 'namespaceId'];
if (evt.category === 'child-workflow')
return ['initiatedEventId', 'startedEventId', 'namespaceId'];
return ['namespaceId'];
})();
const detailFields = fields.filter(
([key, value]) =>
typeof value !== 'object' &&
displayLinkType(key, attrs) === 'none' &&
!hiddenDetailFields.includes(key) &&
(key !== 'namespace' ||
(key === 'namespace' && page.params.namespace !== value)),
);
return {
event: evt,
attributes: attrs,
payloadFields,
detailFields,
linkFields,
};
});
});

const payloadKeyOrder = [
'input',
'result',
'lastFailure',
'failure',
'header',
'searchAttributes',
'memo',
];

const allPayloadFields = $derived.by(() => {
const all = processedEvents.flatMap((p) => p.payloadFields);
return all.toSorted(([a], [b]) => {
const aIndex = payloadKeyOrder.indexOf(a);
const bIndex = payloadKeyOrder.indexOf(b);
const aOrder = aIndex !== -1 ? aIndex : payloadKeyOrder.length - 1;
const bOrder = bIndex !== -1 ? bIndex : payloadKeyOrder.length - 1;
return aOrder - bOrder;
});
});

const hasAnyPayloads = $derived(allPayloadFields.length > 0);
</script>

{#if showEventGroup}
<div class="flex flex-col overflow-hidden">
<div
class="surface-primary flex flex-col overflow-hidden border-t border-subtle"
>
{#if group?.pendingActivity}
<PendingActivityCard activity={group.pendingActivity} />
{:else if group?.pendingNexusOperation}
<PendingNexusOperationCard operation={group.pendingNexusOperation} />
{/if}
{#each group.eventList as groupEvent}
<EventCard event={groupEvent} />
{/each}

{#if hasAnyPayloads}
<div class="flex flex-col border-b border-subtle xl:flex-row">
<div class="flex w-full flex-col xl:w-1/2">
{#each processedEvents as processed, i (processed.event.id)}
<div
class="p-4 {i < processedEvents.length - 1
? 'border-b border-subtle'
: ''}"
>
<div class="flex flex-col gap-1">
<EventDetailsSection
event={processed.event}
{group}
attributes={processed.attributes}
detailFields={processed.detailFields}
linkFields={processed.linkFields}
/>
</div>
</div>
{/each}
</div>
<div class="flex w-full flex-col gap-1 p-4 xl:w-1/2">
<EventPayloads payloadFields={allPayloadFields} />
</div>
</div>
{:else}
{#each processedEvents as processed (processed.event.id)}
<div class="border-b border-subtle p-4">
<div class="flex flex-col gap-1">
<EventDetailsSection
event={processed.event}
{group}
attributes={processed.attributes}
detailFields={processed.detailFields}
linkFields={processed.linkFields}
/>
</div>
</div>
{/each}
{/if}
</div>
{:else if event}
<EventCard {event} />
Expand Down
171 changes: 171 additions & 0 deletions src/lib/components/event/event-details-section.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<script lang="ts">
import { page } from '$app/state';

import Timestamp from '$lib/components/timestamp.svelte';
import Copyable from '$lib/holocene/copyable/index.svelte';
import Link from '$lib/holocene/link.svelte';
import { translate } from '$lib/i18n/translate';
import type { EventGroup } from '$lib/models/event-groups/event-groups';
import type { WorkflowEvent } from '$lib/types/events';
import { getEventLinkHref } from '$lib/utilities/event-link-href';
import {
format,
spaceBetweenCapitalLetters,
} from '$lib/utilities/format-camel-case';
import type { CombinedAttributes } from '$lib/utilities/format-event-attributes';
import { formatDistanceAbbreviated } from '$lib/utilities/format-time';
import {
displayLinkType,
shouldDisplayAsTime,
} from '$lib/utilities/get-single-attribute-for-event';
import { isLocalActivityMarkerEvent } from '$lib/utilities/is-event-type';
import {
routeForEventHistoryEvent,
routeForNamespace,
} from '$lib/utilities/route-for';

import EventDetailsLink from './event-details-link.svelte';
import MetadataDecoder from './metadata-decoder.svelte';

let {
event,
group,
attributes,
detailFields,
linkFields,
}: {
event: WorkflowEvent;
group?: EventGroup;
attributes: CombinedAttributes;
detailFields: [string, unknown][];
linkFields: [string, unknown][];
} = $props();

const { namespace, workflow, run } = $derived(page.params);

const displayName = $derived(
isLocalActivityMarkerEvent(event)
? translate('events.category.local-activity')
: spaceBetweenCapitalLetters(event.name),
);

const durationSinceLastEvent = $derived.by(() => {
if (!group) return '';
const eventIndex = group.eventList.findIndex((evt) => evt.id === event.id);
if (eventIndex === -1 || eventIndex === 0) return '';
const previousEvent = group.eventList[eventIndex - 1];
return formatDistanceAbbreviated({
start: event.eventTime,
end: previousEvent.eventTime,
includeMilliseconds: true,
});
});
</script>

<div class="mb-2 flex w-full flex-wrap items-center justify-between gap-2">
<div class="flex items-center gap-2 text-base">
<Link
href={routeForEventHistoryEvent({
eventId: event.id,
run,
workflow,
namespace,
})}>{event.id}</Link
>
<p class="font-medium">
{displayName}
</p>
</div>
<div class="flex flex-col items-end gap-0 font-mono text-sm leading-4">
<Timestamp as="p" dateTime={event.eventTime} />
{#if durationSinceLastEvent}
<p class="font-medium text-secondary">+{durationSinceLastEvent}</p>
{/if}
</div>
</div>

{#if event?.links?.length}
{#each event.links as link}

Check warning on line 88 in src/lib/components/event/event-details-section.svelte

View workflow job for this annotation

GitHub Actions / lint

Each block should have a key
{@const href = getEventLinkHref(link)}
{@const value = href.split('workflows/')?.[1] || href}
<div class="flex items-start gap-4">
<p class="min-w-56 text-sm font-medium text-secondary">
{translate('nexus.link')}
</p>
<Copyable
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
content={value}
>
<Link {href} class="whitespace-pre-line">{value}</Link>
</Copyable>
</div>
{@const nsHref = routeForNamespace({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ 'link.workflowEvent' is possibly 'null' or 'undefined'.
  • ⚠️ Type 'string | null | undefined' is not assignable to type 'string'.

namespace: link.workflowEvent.namespace,
})}
<div class="flex items-start gap-4">
<p class="min-w-56 text-sm font-medium text-secondary">
{translate('nexus.link-namespace')}
</p>
<Copyable
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ 'link.workflowEvent' is possibly 'null' or 'undefined'.
  • ⚠️ Type 'string | null | undefined' is not assignable to type 'string'.

content={link.workflowEvent.namespace}
>
<Link href={nsHref} class="whitespace-pre-line"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ 'link.workflowEvent' is possibly 'null' or 'undefined'.

>{link.workflowEvent.namespace}</Link
>
</Copyable>
</div>
{/each}
{/if}

{#if event?.userMetadata?.summary}
<div class="flex items-start gap-4">
<p class="min-w-56 text-sm font-medium text-secondary">Summary</p>
<p class="whitespace-pre-line">
<MetadataDecoder
value={event.userMetadata.summary}
let:decodedValue
fallback={translate('events.decode-failed')}
>
{decodedValue}
</MetadataDecoder>
</p>
</div>
{/if}

{#each detailFields as [key, value] (key)}
<div class="flex items-start gap-4">
<p class="min-w-56 text-sm font-medium text-secondary">
{format(key)}
</p>
<p class="whitespace-pre-line break-all">
{#if shouldDisplayAsTime(key)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Type 'unknown' is not assignable to type 'ValidTime | null | undefined'.

<Timestamp dateTime={value} />
{:else}
{value}
{/if}
</p>
</div>
{/each}

{#each linkFields as [key, value] (key)}
<div class="flex items-start gap-4">
<p class="min-w-56 text-sm font-medium text-secondary">
{format(key)}
</p>
<Copyable
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
content={String(value)}
>
<EventDetailsLink
value={String(value)}
{attributes}
type={displayLinkType(key, attributes)}
class="whitespace-pre-line"
/>
</Copyable>
</div>
{/each}
75 changes: 75 additions & 0 deletions src/lib/components/event/event-payloads.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script lang="ts">
import CodeBlock from '$lib/holocene/code-block.svelte';
import { translate } from '$lib/i18n/translate';
import { format } from '$lib/utilities/format-camel-case';
import {
getCodeBlockValue,
getStackTrace,
} from '$lib/utilities/get-single-attribute-for-event';

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

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let { payloadFields }: { payloadFields: [string, any][] } = $props();
</script>

{#each payloadFields as [key, value] (key)}
{@const codeBlockValue = getCodeBlockValue(value)}
{@const stackTrace = getStackTrace(codeBlockValue)}
<div>
<p class="mb-1 min-w-56 text-sm font-medium text-secondary">
{format(key)}
</p>
{#if value?.payloads}
<PayloadDecoder {value} key="payloads">
{#snippet children(decodedValue)}
<CodeBlock
content={decodedValue}
maxHeight={384}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
/>
{/snippet}
</PayloadDecoder>
{:else if key === 'searchAttributes'}
<PayloadDecoder
key="searchAttributes"
value={{ searchAttributes: codeBlockValue }}
>
{#snippet children(decodedValue)}
<CodeBlock
content={decodedValue}
maxHeight={384}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
/>
{/snippet}
</PayloadDecoder>
{:else}
<PayloadDecoder value={codeBlockValue}>
{#snippet children(decodedValue)}
<CodeBlock
content={decodedValue}
maxHeight={384}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
/>
{/snippet}
</PayloadDecoder>
{/if}
</div>
{#if stackTrace}
<div>
<p class="mb-1 min-w-56 text-sm font-medium text-secondary">
{translate('workflows.call-stack-tab')}
</p>
<CodeBlock
content={stackTrace}
language="text"
maxHeight={384}
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
/>
</div>
{/if}
{/each}
2 changes: 1 addition & 1 deletion src/lib/components/event/event-summary-row.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@
{#if isEventGroup(event)}
<td class="font-mono">
<div class="flex items-center gap-0.5">
{#each event.eventList as groupEvent}

Check warning on line 220 in src/lib/components/event/event-summary-row.svelte

View workflow job for this annotation

GitHub Actions / lint

Each block should have a key
<Link
data-testid="link"
href={routeForEventHistoryEvent({
Expand Down Expand Up @@ -380,7 +380,7 @@
</tr>
{#if expanded}
<tr
class="w-full text-sm no-underline"
class="w-full text-sm no-underline hover:!bg-transparent"
data-testid="event-summary-row-expanded"
>
<td class="!p-0" colspan={$isCloud ? 5 : 4}>
Expand Down
Loading
Loading