Repository: temporalio/ui
Pull request #301 (2022-03-11 02:14:22)

Simplify event history
This PR simplifies the event history routing to a single view and removes a bunch of complexity.

It also allows you to navigate to related links as you would in the compact view.

cloudflare-pages[bot]: ## Deploying with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>9c83d26</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://8c4271ef.ui-c3x.pages.dev'>https://8c4271ef.ui-c3x.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/temporal-pizza/8c4271ef-0c81-4bef-832f-e480b2b67dc1)


Patch:
diff --git a/src/app.postcss b/src/app.postcss
index 9bbc8480..4b34bf98 100644
--- a/src/app.postcss
+++ b/src/app.postcss
@@ -20,3 +20,7 @@ body {
   max-height: 25em;
   overflow-y: scroll;
 }
+
+select {
+  -webkit-appearance: none;
+}
diff --git a/src/events.d.ts b/src/events.d.ts
index 4cf56239..21af9ff8 100644
--- a/src/events.d.ts
+++ b/src/events.d.ts
@@ -8,14 +8,25 @@ type TimerType = import('$lib/utilities/is-event-type').TimerType;
 type SignalType = import('$lib/utilities/is-event-type').SignalType;
 type MarkerType = import('$lib/utilities/is-event-type').MarkerType;
 type ChildType = import('$lib/utilities/is-event-type').ChildType;
-type EventTypeCategory = import('$lib/utilities/get-event-categorization');
+
+type EventTypeCategory =
+  import('$lib/models/event-history/get-event-categorization').EventTypeCategory;
+
+type EventClassification =
+  import('$lib/utilities/get-event-classiciation').EventClassification;
 
 interface HistoryEventWithId extends HistoryEvent {
   id: string;
   eventType: EventType;
   attributes: EventAttribute;
+  timestamp: string;
+  classification: EventClassification;
+  category: EventTypeCategory;
+  name: EventType;
 }
 
+type HistoryEvents = HistoryEventWithId[];
+
 interface PendingActivity extends PendingActivityInfo {
   id: typeof PendingActivityInfo.activityId;
   state: 'Unspecified' | 'Scheduled' | 'Started' | 'CancelRequested';
@@ -29,7 +40,11 @@ type CommonEventKey =
   | 'eventId'
   | 'eventTime'
   | 'version'
-  | 'taskId';
+  | 'taskId'
+  | 'timestamp'
+  | 'classification'
+  | 'category'
+  | 'name';
 
 type CommonHistoryEvent = Pick<HistoryEventWithId, CommonEventKey>;
 
@@ -144,5 +159,9 @@ type ChildEvent = StartChildWorkflowExecutionInitiatedEvent &
   ChildWorkflowExecutionTimedOutEvent &
   ChildWorkflowExecutionTerminatedEvent;
 
-type EventsOrActivities = (HistoryEventWithId | PendingActivity)[];
-type IterableEvents = EventsOrActivities | CompactEventGroups;
+type EventHistoryView = 'full' | 'compact' | 'json';
+
+type FetchEventsResponse = {
+  events: HistoryEventWithId[];
+  eventGroups: CompactEventGroups;
+};
diff --git a/src/lib/components/code-block.svelte b/src/lib/components/code-block.svelte
index 25bb7281..49f82d71 100644
--- a/src/lib/components/code-block.svelte
+++ b/src/lib/components/code-block.svelte
@@ -47,8 +47,7 @@
     <!-- The spacing for this if statement is like this because PRE's honor all whitespace and 
       line breaks so we have this peculiar formatting to preserve this components output -->
     <pre
-      class="p-4"
-      class:rounded-2xl={framed}
+      class="p-4 rounded-lg"
       class:h-full={framed}><code class="language-{language}"
       >{#if isJSON}{formatJSON(content)}{:else}{@html content}{/if}</code></pre>
 
@@ -64,6 +63,6 @@
 
 <style lang="postcss">
   .framed {
-    @apply border-2 border-gray-300 rounded-2xl p-4 flex flex-col;
+    @apply border-2 border-gray-300 rounded-lg p-4 flex flex-col;
   }
 </style>
diff --git a/src/lib/components/event-label.svelte b/src/lib/components/event-label.svelte
index d358247b..9049365b 100644
--- a/src/lib/components/event-label.svelte
+++ b/src/lib/components/event-label.svelte
@@ -1,6 +1,4 @@
 <script lang="ts">
-  import type { EventClassification } from '$lib/utilities/get-event-classification';
-
   export let color: EventClassification | PendingActivity['state'] = null;
 </script>
 
diff --git a/src/lib/components/event-table.svelte b/src/lib/components/event-table.svelte
deleted file mode 100644
index 6dd71d6a..00000000
--- a/src/lib/components/event-table.svelte
+++ /dev/null
@@ -1,50 +0,0 @@
-<script lang="ts">
-  import Pagination from './pagination.svelte';
-  import Event from './event.svelte';
-
-  export let events: IterableEvents;
-</script>
-
-<Pagination items={[...events]} let:visibleItems>
-  <section
-    class="flex flex-col border-2 border-gray-300 rounded-lg w-full event-history mb-6"
-  >
-    <div class="flex w-full">
-      <header class="table-header border-r-2 rounded-tl-lg w-1/3">
-        <h3>Summary</h3>
-        <slot name="filters" />
-      </header>
-      <header class="table-header rounded-tr-lg w-2/3">
-        <h3>Details</h3>
-      </header>
-    </div>
-    <div class="flex h-full overflow-y-hidden">
-      <div
-        class="flex flex-col h-full w-1/3 border-r-2 border-gray-300 rounded-bl-lg"
-      >
-        <div class="rounded-bl-lg overflow-y-scroll h-screen">
-          {#each visibleItems as event (event.id)}
-            <Event {event} />
-          {/each}
-        </div>
-      </div>
-      <div class="flex flex-col h-full w-2/3">
-        <div
-          class="h-full  overflow-y-scroll overflow-x-hidden rounded-br-lg px-4"
-        >
-          <slot name="details">
-            <div class="flex items-center justify-center w-full h-full">
-              <p class="text-gray-600 italic">(Nothing selected.)</p>
-            </div>
-          </slot>
-        </div>
-      </div>
-    </div>
-  </section>
-</Pagination>
-
-<style lang="postcss">
-  .table-header {
-    @apply bg-gray-100 text-gray-800 font-semibold p-4 flex justify-between items-center border-b-2;
-  }
-</style>
diff --git a/src/lib/components/event.svelte b/src/lib/components/event.svelte
index 6773eba9..31f6eb98 100644
--- a/src/lib/components/event.svelte
+++ b/src/lib/components/event.svelte
@@ -1,51 +1,49 @@
 <script lang="ts">
   import Icon from 'svelte-fa';
   import { faCalendar } from '@fortawesome/free-solid-svg-icons';
-  import { page } from '$app/stores';
 
-  import { formatEvent } from '$lib/utilities/get-event-classification';
-  import { formatDate } from '$lib/utilities/format-date';
+  import { page } from '$app/stores';
+  import { isEvent } from '$lib/models/event-history';
+  import { isEventGroup } from '$lib/models/group-events';
 
   import EventLabel from '$lib/components/event-label.svelte';
 
-  export let event: HistoryEventWithId | PendingActivity | CompactEventGroup;
+  export let event: HistoryEventWithId | CompactEventGroup;
 
-  let { routeFor, pending, timeStamp, name, tag, classification, id } =
-    formatEvent(event);
+  const isActive = (currentId: string): boolean => {
+    if (isEvent(event)) {
+      return event.id === currentId;
+    }
 
-  let { namespace, workflow: workflowId, run: runId } = $page.params;
-  let href = routeFor({
-    namespace,
-    workflowId,
-    runId,
-    query: $page.url.searchParams,
-  });
+    if (isEventGroup(event)) {
+      return event.eventIds.has(currentId);
+    }
+  };
+
+  $: href = $page.params.eventId
+    ? event.id
+    : `${$page.url.pathname}/${event.id}`;
 </script>
 
 <a
   {href}
   sveltekit:noscroll
-  sveltekit:prefetch
   class="flex border-b-2 border-gray-300 w-full items-center hover:bg-gray-50"
-  class:pending
-  class:active={$page.params.id === event.id}
+  class:active={isActive($page.params.eventId)}
 >
   <article class="flex gap-4 items-center p-4 w-full">
-    <p class="w-5 text-center text-gray-500">{id}</p>
+    <p class="w-5 text-center text-gray-500">{event.id}</p>
     <div class="w-full">
-      <h2 class="mb-2 {tag}">
-        <EventLabel color={classification}>
-          {name}
+      <h2 class="mb-2">
+        <EventLabel color={event.classification}>
+          {event.name}
         </EventLabel>
       </h2>
       <p class="text-sm">
         <Icon icon={faCalendar} class="inline" />
-        {formatDate(timeStamp)}
+        {event.timestamp}
       </p>
     </div>
-    {#if pending}
-      <div class="mx-8 text-orange-600 italic">Pending</div>
-    {/if}
   </article>
 </a>
 
@@ -62,14 +60,6 @@
     );
   }
 
-  .pending {
-    @apply bg-orange-50;
-  }
-
-  .pending .active {
-    @apply bg-blue-50;
-  }
-
   a:last-child {
     @apply border-b-0;
   }
diff --git a/src/lib/components/filter-checkbox.svelte b/src/lib/components/filter-checkbox.svelte
new file mode 100644
index 00000000..58caef7c
--- /dev/null
+++ b/src/lib/components/filter-checkbox.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+  import debounce from 'just-debounce';
+
+  import { goto } from '$app/navigation';
+  import { page } from '$app/stores';
+  import { updateQueryParameters } from '$lib/utilities/update-query-parameters';
+
+  export let parameter: string;
+  export let name: string;
+  export let value: boolean;
+
+  let _value: boolean =
+    !!(parameter && $page.url.searchParams.get(parameter)) || value;
+
+  const id = `${parameter || name}-filter`;
+  const update = debounce(updateQueryParameters, 300);
+
+  $: {
+    update({
+      parameter,
+      value: _value,
+      query: $page.url.searchParams,
+      path: $page.url.pathname,
+      goto,
+    });
+  }
+</script>
+
+<div>
+  <input type="checkbox" {id} bind:checked={_value} />
+  <label for={id}>{name}</label>
+</div>
diff --git a/src/lib/components/input-and-result.svelte b/src/lib/components/input-and-result.svelte
index ee4c15bf..4794a5aa 100644
--- a/src/lib/components/input-and-result.svelte
+++ b/src/lib/components/input-and-result.svelte
@@ -4,7 +4,7 @@
 
   export let events: HistoryEventWithId[];
 
-  $: inputAndResult = getWorkflowStartedAndCompletedEvents(events);
+  let inputAndResult = getWorkflowStartedAndCompletedEvents(events);
 </script>
 
 <section class="flex flex-col gap-4">
diff --git a/src/lib/models/event-history.ts b/src/lib/models/event-history.ts
deleted file mode 100644
index 2db9275e..00000000
--- a/src/lib/models/event-history.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { convertPayloadToJson } from '$lib/utilities/decode-payload';
-import { findAttributesAndKey } from '$lib/utilities/is-event-type';
-
-import type { GetWorkflowExecutionHistoryResponse } from '$types';
-
-export async function getEventAttributes(
-  historyEvent: HistoryEvent,
-): Promise<EventAttributesWithType> {
-  const { key, attributes } = findAttributesAndKey(historyEvent);
-  const decodedAttributes = await convertPayloadToJson(attributes);
-
-  return {
-    type: key,
-    ...decodedAttributes,
-  };
-}
-
-export const toEventHistory = async (
-  response: GetWorkflowExecutionHistoryResponse,
-): Promise<HistoryEventWithId[]> => {
-  return Promise.all(
-    response.history.events.map(
-      async (event): Promise<HistoryEventWithId> => ({
-        ...event,
-        id: String(event.eventId),
-        eventType: event.eventType as unknown as EventType,
-        attributes: await getEventAttributes(event),
-      }),
-    ),
-  );
-};
diff --git a/src/lib/utilities/get-event-categorization.test.ts b/src/lib/models/event-history/get-event-categorization.test.ts
similarity index 100%
rename from src/lib/utilities/get-event-categorization.test.ts
rename to src/lib/models/event-history/get-event-categorization.test.ts
diff --git a/src/lib/utilities/get-event-categorization.ts b/src/lib/models/event-history/get-event-categorization.ts
similarity index 94%
rename from src/lib/utilities/get-event-categorization.ts
rename to src/lib/models/event-history/get-event-categorization.ts
index fec1da70..c7a55006 100644
--- a/src/lib/utilities/get-event-categorization.ts
+++ b/src/lib/models/event-history/get-event-categorization.ts
@@ -58,6 +58,10 @@ const categories = [
   'command',
 ] as const;
 
+export const getEventCategory = (eventType: EventType): EventTypeCategory => {
+  return eventTypeCategorizations[eventType];
+};
+
 export const eventTypeInCategory =
   (category?: EventTypeCategory) =>
   (event: HistoryEventWithId): boolean => {
diff --git a/src/lib/models/event-history/get-event-classification.ts b/src/lib/models/event-history/get-event-classification.ts
new file mode 100644
index 00000000..0301710b
--- /dev/null
+++ b/src/lib/models/event-history/get-event-classification.ts
@@ -0,0 +1,29 @@
+export type EventClassification = typeof eventClassifications[number];
+
+export const eventClassifications = [
+  'Unspecified',
+  'Scheduled',
+  'Open',
+  'New',
+  'Started',
+  'Initiated',
+  'Running',
+  'Completed',
+  'Fired',
+  'CancelRequested',
+  'TimedOut',
+  'Signaled',
+  'Canceled',
+  'Failed',
+  'Terminated',
+] as const;
+
+export const getEventClassification = (
+  eventType: EventType,
+): EventClassification => {
+  if (eventType.includes('RequestCancel')) return 'CancelRequested';
+
+  for (const classification of eventClassifications) {
+    if (eventType.includes(classification)) return classification;
+  }
+};
diff --git a/src/lib/models/event-history/index.ts b/src/lib/models/event-history/index.ts
new file mode 100644
index 00000000..35985e90
--- /dev/null
+++ b/src/lib/models/event-history/index.ts
@@ -0,0 +1,62 @@
+import { convertPayloadToJson } from '$lib/utilities/decode-payload';
+import { formatDate } from '$lib/utilities/format-date';
+import { has } from '$lib/utilities/has';
+import { findAttributesAndKey } from '$lib/utilities/is-event-type';
+
+import { groupEvents } from '../group-events';
+import { getEventCategory } from './get-event-categorization';
+import { getEventClassification } from './get-event-classification';
+
+export async function getEventAttributes(
+  historyEvent: HistoryEvent,
+): Promise<EventAttributesWithType> {
+  const { key, attributes } = findAttributesAndKey(historyEvent);
+  const decodedAttributes = await convertPayloadToJson(attributes);
+
+  return {
+    type: key,
+    ...decodedAttributes,
+  };
+}
+
+const toEvent = async (
+  historyEvent: HistoryEvent,
+): Promise<HistoryEventWithId> => {
+  const id = String(historyEvent.eventId);
+  const eventType = historyEvent.eventType as unknown as EventType;
+  const timestamp = formatDate(String(historyEvent.eventTime));
+  const classification = getEventClassification(eventType);
+  const category = getEventCategory(eventType);
+  const attributes = await getEventAttributes(historyEvent);
+
+  return {
+    ...historyEvent,
+    attributes,
+    eventType,
+    classification,
+    category,
+    id,
+    name: eventType,
+    timestamp,
+  };
+};
+
+export const toEventHistory = async (
+  response: HistoryEvent[],
+): Promise<{
+  events: HistoryEventWithId[];
+  eventGroups: CompactEventGroups;
+}> => {
+  const events = await Promise.all(response.map(toEvent));
+  const eventGroups = groupEvents(events);
+
+  return { events, eventGroups };
+};
+
+export const isEvent = (event: unknown): event is HistoryEventWithId => {
+  if (event === null) return false;
+  if (typeof event !== 'object') return false;
+  if (Array.isArray(event)) return false;
+  if (has(event, 'eventType')) return true;
+  return false;
+};
diff --git a/src/lib/models/group-events/create-event-group.ts b/src/lib/models/group-events/create-event-group.ts
index d5bab810..1c2b9555 100644
--- a/src/lib/models/group-events/create-event-group.ts
+++ b/src/lib/models/group-events/create-event-group.ts
@@ -1,4 +1,3 @@
-import { getName } from '$lib/utilities/get-event-name';
 import { getGroupId } from './get-group-id';
 
 import {
@@ -9,6 +8,30 @@ import {
   isTimerStartedEvent,
 } from '$lib/utilities/is-event-type';
 
+export const getName = (event: CommonHistoryEvent): string => {
+  if (!event) return;
+
+  if (isActivityTaskScheduledEvent(event)) {
+    return event.activityTaskScheduledEventAttributes?.activityType?.name;
+  }
+
+  if (isTimerStartedEvent(event)) {
+    return `Timer ${event.timerStartedEventAttributes?.timerId} (${event.timerStartedEventAttributes?.startToFireTimeout})`;
+  }
+
+  if (isSignalExternalWorkflowExecutionInitiatedEvent(event)) {
+    return `Signal: ${event.signalExternalWorkflowExecutionInitiatedEventAttributes?.signalName}`;
+  }
+
+  if (isMarkerRecordedEvent(event)) {
+    return `Marker: ${event.markerRecordedEventAttributes?.markerName}`;
+  }
+
+  if (isStartChildWorkflowExecutionInitiatedEvent(event)) {
+    return `Child Workflow: ${event.startChildWorkflowExecutionInitiatedEventAttributes?.workflowType?.name}`;
+  }
+};
+
 type StartingEvents = {
   Activity: ActivityTaskScheduledEvent;
   ChildWorkflow: StartChildWorkflowExecutionInitiatedEvent;
@@ -22,12 +45,26 @@ const createGroupFor = <K extends keyof StartingEvents>(
 ): CompactEventGroup => {
   const id = getGroupId(event);
   const name = getName(event);
+  const { timestamp, category, classification } = event;
+
+  const initialEvent = event;
 
-  const events = new Map<EventType, HistoryEventWithId>();
+  const events: CompactEventGroup['events'] = new Map();
+  const eventIds: CompactEventGroup['eventIds'] = new Set();
 
-  events.set(event.eventType, event);
+  events.set(event.id, event);
+  eventIds.add(event.id);
 
-  return { id, name, events };
+  return {
+    id,
+    name,
+    events,
+    eventIds,
+    initialEvent,
+    timestamp,
+    category,
+    classification,
+  };
 };
 
 export const createEventGroup = (event: CommonHistoryEvent) => {
diff --git a/src/lib/models/group-events/event-groups.d.ts b/src/lib/models/group-events/event-groups.d.ts
index c303980d..d07873fb 100644
--- a/src/lib/models/group-events/event-groups.d.ts
+++ b/src/lib/models/group-events/event-groups.d.ts
@@ -1,8 +1,11 @@
-type CompactEventGroup<T = EventType, E = HistoryEventWithId> = {
-  id: string;
+type CompactEventGroup<T = EventType, E = HistoryEventWithId, Id = E['id']> = {
+  id: Id;
   name: string;
-  events: Map<T, E>;
-};
+  events: Map<Id, E>;
+  eventIds: Set<Id>;
+  initialEvent: HistoryEventWithId;
+  timestamp: HistoryEventWithId['timestamp'];
+} & Pick<HistoryEventWithId, 'timestamp' | 'classification' | 'category'>;
 
 type CompactEventGroups<
   T = EventType,
diff --git a/src/lib/models/group-events/get-group-for-event.ts b/src/lib/models/group-events/get-group-for-event.ts
new file mode 100644
index 00000000..585099cc
--- /dev/null
+++ b/src/lib/models/group-events/get-group-for-event.ts
@@ -0,0 +1,15 @@
+export const getGroupForEvent = (
+  event: HistoryEventWithId,
+  groups: CompactEventGroups,
+): CompactEventGroup => {
+  const eventId = event.id;
+
+  for (const group of groups) {
+    if (eventId === group.id) return group;
+    for (const id of group.eventIds) {
+      if (eventId === id) {
+        return group;
+      }
+    }
+  }
+};
diff --git a/src/lib/models/group-events/index.ts b/src/lib/models/group-events/index.ts
index ccc0b12f..0c3f7407 100644
--- a/src/lib/models/group-events/index.ts
+++ b/src/lib/models/group-events/index.ts
@@ -1,6 +1,21 @@
+import { has } from '$lib/utilities/has';
 import { createEventGroup } from './create-event-group';
 import { getGroupId } from './get-group-id';
 
+export { getGroupForEvent } from './get-group-for-event';
+
+const addToExistingGroup = (
+  group: CompactEventGroup,
+  event: HistoryEventWithId,
+): void => {
+  if (!group) return;
+
+  group.events.set(event.id, event);
+  group.eventIds.add(event.id);
+
+  group.timestamp = event.timestamp;
+};
+
 export const groupEvents = (
   events: CommonHistoryEvent[],
 ): CompactEventGroups => {
@@ -13,9 +28,21 @@ export const groupEvents = (
     if (group) {
       groups[group.id] = group;
     } else {
-      groups[id]?.events.set(event.eventType, event);
+      addToExistingGroup(groups[id], event);
     }
   }
 
   return Object.values(groups);
 };
+
+export const isEventGroup = (
+  eventOrGroup: unknown,
+): eventOrGroup is CompactEventGroup => {
+  return has(eventOrGroup, 'events');
+};
+
+export const isEventGroups = (
+  eventsOrGroups: unknown[],
+): eventsOrGroups is CompactEventGroups => {
+  return eventsOrGroups.every(isEventGroup);
+};
diff --git a/src/lib/services/events-service.ts b/src/lib/services/events-service.ts
index dd8320a1..a54b6894 100644
--- a/src/lib/services/events-service.ts
+++ b/src/lib/services/events-service.ts
@@ -12,31 +12,11 @@ type FetchEvents = NamespaceScopedRequest &
     rawPayloads?: boolean;
   };
 
-export const fetchEvents = async (
-  { namespace, executionId, runId, onStart, onUpdate, onComplete }: FetchEvents,
-  request = fetch,
-): Promise<HistoryEventWithId[]> => {
-  const events = await paginated(
-    async (token: string) => {
-      return requestFromAPI<GetWorkflowExecutionHistoryResponse>(
-        routeForApi('events', { namespace, executionId, runId }),
-        {
-          token,
-          request,
-        },
-      );
-    },
-    { onStart, onUpdate, onComplete },
-  ).then(toEventHistory);
-
-  return events;
-};
-
 export const fetchRawEvents = async (
   { namespace, executionId, runId, onStart, onUpdate, onComplete }: FetchEvents,
   request = fetch,
 ): Promise<HistoryEvent[]> => {
-  const resp = await paginated(
+  const response = await paginated(
     async (token: string) => {
       return requestFromAPI<GetWorkflowExecutionHistoryResponse>(
         routeForApi('events', { namespace, executionId, runId }),
@@ -49,5 +29,12 @@ export const fetchRawEvents = async (
     { onStart, onUpdate, onComplete },
   );
 
-  return resp.history.events;
+  return response.history.events;
+};
+
+export const fetchEvents = async (
+  parameters: FetchEvents,
+  request = fetch,
+): Promise<FetchEventsResponse> => {
+  return fetchRawEvents(parameters, request).then(toEventHistory);
 };
diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts
index 88e34f87..a64e0b25 100644
--- a/src/lib/utilities/decode-payload.ts
+++ b/src/lib/utilities/decode-payload.ts
@@ -3,6 +3,7 @@ import { dataConverterWebsocket } from '$lib/utilities/data-converter-websocket'
 import type { DataConverterWebsocketInterface } from '$lib/utilities/data-converter-websocket';
 
 import { convertPayload } from '$lib/services/data-converter';
+
 import { atob } from './atob';
 
 export function decodePayload(
diff --git a/src/lib/utilities/get-event-classification.ts b/src/lib/utilities/get-event-classification.ts
deleted file mode 100644
index d0573c1d..00000000
--- a/src/lib/utilities/get-event-classification.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { format } from './format-camel-case';
-import { routeFor } from './route-for';
-import { getLastEvent } from '$lib/models/group-events/get-last-event';
-
-import type { WorkflowParameters } from './route-for';
-import type { Timestamp } from '$types';
-
-export type EventClassification = typeof eventClassifications[number];
-type EventOrGroup = HistoryEventWithId | PendingActivity | CompactEventGroup;
-
-type EventSummary = {
-  id: string;
-  name: string;
-  timeStamp: string;
-  classification: EventClassification;
-  tag: string;
-  pending: boolean;
-  activity: boolean;
-  type: 'event' | 'pending-activity' | 'activity';
-  routeFor: (parameters: WorkflowParameters) => string;
-};
-
-export const eventClassifications = [
-  'Unspecified',
-  'Scheduled',
-  'Open',
-  'New',
-  'Started',
-  'Initiated',
-  'Running',
-  'Completed',
-  'Fired',
-  'CancelRequested',
-  'TimedOut',
-  'Signaled',
-  'Canceled',
-  'Failed',
-  'Terminated',
-] as const;
-
-const has = (target: unknown, property: string): boolean => {
-  return Object.prototype.hasOwnProperty.call(target, property);
-};
-
-export const isEvent = (event: unknown): event is HistoryEventWithId => {
-  if (event === null) return false;
-  if (typeof event !== 'object') return false;
-  if (Array.isArray(event)) return false;
-  if (has(event, 'eventType')) return true;
-  return false;
-};
-
-export const isPendingActivity = (
-  event: EventOrGroup,
-): event is PendingActivity => {
-  if (event === null) return false;
-  if (typeof event !== 'object') return false;
-  if (Array.isArray(event)) return false;
-  if (has(event, 'activityType')) return true;
-  return false;
-};
-
-export const isEventGroup = (
-  eventOrGroup: EventOrGroup,
-): eventOrGroup is CompactEventGroup => {
-  return has(eventOrGroup, 'events');
-};
-
-export const isEventGroups = (
-  eventsOrGroups: unknown[],
-): eventsOrGroups is CompactEventGroups => {
-  return eventsOrGroups.every(isEventGroup);
-};
-
-export const getEventClassification = (
-  event: EventOrGroup,
-): EventClassification => {
-  if (isPendingActivity(event)) return event.state;
-  if (isEventGroup(event)) {
-    event =
-      event.events.get('ActivityTaskScheduled') ??
-      event.events.get('TimerStarted') ??
-      event.events.get('WorkflowExecutionSignaled') ??
-      event.events.get('MarkerRecorded') ??
-      event.events.get('StartChildWorkflowExecutionInitiated');
-  }
-
-  const eventType = event.eventType;
-
-  if (eventType.includes('RequestCancel')) return 'CancelRequested';
-  for (const classification of eventClassifications) {
-    if (eventType.includes(classification)) return classification;
-  }
-};
-
-const getName = (event: EventOrGroup): string => {
-  if (isEvent(event)) return String(event.eventType);
-  if (isPendingActivity(event))
-    return `${event.activityType.name}:${event.state}`;
-  if (isEventGroup(event)) return event.name;
-};
-
-const getTime = (event: EventOrGroup): string => {
-  let ts: Timestamp;
-
-  if (isEvent(event)) ts = event.eventTime;
-  if (isPendingActivity(event)) ts = event.lastStartedTime;
-  if (isEventGroup(event)) ts = getLastEvent(event).eventTime;
-
-  return ts ? String(ts) : null;
-};
-
-const getId = (event: EventOrGroup): string => {
-  if (isEvent(event)) return String(event.eventId);
-  if (isPendingActivity(event)) return String(event.activityId);
-  if (isEventGroup(event)) return String(event.id);
-};
-
-const getType = (
-  event: EventOrGroup,
-): 'event' | 'pending-activity' | 'activity' => {
-  if (isEvent(event)) return 'event';
-  if (isPendingActivity(event)) return 'pending-activity';
-  if (isEventGroup(event)) return 'activity';
-};
-
-export const getHref = (
-  event: EventOrGroup | PendingActivity,
-  parameters: WorkflowParameters,
-): string => {
-  if (isEvent(event)) {
-    const route = routeFor('workflow.events.full.event', {
-      ...parameters,
-      eventId: String(event.eventId),
-    });
-
-    return route;
-  }
-
-  if (isPendingActivity(event)) {
-    return routeFor('workflow.events.full.pending', {
-      ...parameters,
-      eventId: String(event.activityId),
-    });
-  }
-
-  if (isEventGroup(event)) {
-    return routeFor('workflow.events.compact.activity', {
-      ...parameters,
-      eventId: String(event.id),
-    });
-  }
-};
-
-export const formatEvent = (event: EventOrGroup): EventSummary => {
-  return {
-    id: getId(event),
-    name: format(getName(event)),
-    timeStamp: getTime(event),
-    classification: getEventClassification(event),
-    tag: getName(event),
-    type: getType(event),
-    pending: isPendingActivity(event),
-    activity: isEventGroup(event),
-    routeFor: (parameters: WorkflowParameters) => getHref(event, parameters),
-  };
-};
diff --git a/src/lib/utilities/get-event-name.ts b/src/lib/utilities/get-event-name.ts
deleted file mode 100644
index c9d82d5a..00000000
--- a/src/lib/utilities/get-event-name.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {
-  isActivityTaskScheduledEvent,
-  isStartChildWorkflowExecutionInitiatedEvent,
-  isTimerStartedEvent,
-  isSignalExternalWorkflowExecutionInitiatedEvent,
-  isMarkerRecordedEvent,
-} from '$lib/utilities/is-event-type';
-
-export const getName = (event: CommonHistoryEvent): string => {
-  if (!event) return;
-
-  if (isActivityTaskScheduledEvent(event)) {
-    return event.activityTaskScheduledEventAttributes?.activityType?.name;
-  }
-
-  if (isTimerStartedEvent(event)) {
-    return `Timer ${event.timerStartedEventAttributes?.timerId} (${event.timerStartedEventAttributes?.startToFireTimeout})`;
-  }
-
-  if (isSignalExternalWorkflowExecutionInitiatedEvent(event)) {
-    return `Signal: ${event.signalExternalWorkflowExecutionInitiatedEventAttributes?.signalName}`;
-  }
-
-  if (isMarkerRecordedEvent(event)) {
-    return `Marker: ${event.markerRecordedEventAttributes?.markerName}`;
-  }
-
-  if (isStartChildWorkflowExecutionInitiatedEvent(event)) {
-    return `Child Workflow: ${event.startChildWorkflowExecutionInitiatedEventAttributes?.workflowType?.name}`;
-  }
-};
diff --git a/src/lib/utilities/get-visible-events.ts b/src/lib/utilities/get-visible-events.ts
deleted file mode 100644
index 5e519096..00000000
--- a/src/lib/utilities/get-visible-events.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { eventTypeInCategory } from './get-event-categorization';
-import { isEventGroups } from './get-event-classification';
-
-export function getVisibleEvents(
-  events: CompactEventGroups,
-  workflow: never,
-  category: never,
-): CompactEventGroups;
-
-export function getVisibleEvents(
-  events: HistoryEventWithId[],
-  workflow: WorkflowExecution,
-  category: EventTypeCategory,
-): EventsOrActivities;
-
-export function getVisibleEvents(
-  events: HistoryEventWithId[] | CompactEventGroups,
-  workflow: WorkflowExecution,
-  category: EventTypeCategory,
-): IterableEvents {
-  if (isEventGroups(events)) return events;
-
-  const pendingActivities = workflow.pendingActivities;
-  const visibleEvents = events.filter(eventTypeInCategory(category));
-
-  return [...pendingActivities, ...visibleEvents];
-}
diff --git a/src/lib/utilities/has.ts b/src/lib/utilities/has.ts
new file mode 100644
index 00000000..602c0793
--- /dev/null
+++ b/src/lib/utilities/has.ts
@@ -0,0 +1,3 @@
+export const has = (target: unknown, property: string): boolean => {
+  return Object.prototype.hasOwnProperty.call(target, property);
+};
diff --git a/src/lib/utilities/is-pending-activity.ts b/src/lib/utilities/is-pending-activity.ts
new file mode 100644
index 00000000..5fda86c0
--- /dev/null
+++ b/src/lib/utilities/is-pending-activity.ts
@@ -0,0 +1,9 @@
+import { has } from './has';
+
+export const isPendingActivity = (event: unknown): event is PendingActivity => {
+  if (event === null) return false;
+  if (typeof event !== 'object') return false;
+  if (Array.isArray(event)) return false;
+  if (has(event, 'activityType')) return true;
+  return false;
+};
diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts
index 6217d6eb..8ea43b2a 100644
--- a/src/lib/utilities/route-for.ts
+++ b/src/lib/utilities/route-for.ts
@@ -5,18 +5,11 @@ type RoutePath =
   | 'workers'
   | 'workflow.events.full'
   | 'workflow.events.full.event'
-  | 'workflow.events.full.pending'
-  | 'workflow.events.compact'
-  | 'workflow.events.compact.activity'
-  | 'workflow.events.compact.activity.event'
   | 'workflow.events.json'
   | 'workflow.stack-trace'
   | 'workflow.query'
   | 'workers';
 
-type EventView = 'event' | 'pending' | 'activity';
-type EventHistoryView = 'full' | 'compact' | 'json';
-
 export type QueryParameters = { query?: URLSearchParams };
 export type NamespaceParameter = { namespace: string } & QueryParameters;
 export type WorkflowParameters = {
@@ -57,35 +50,11 @@ const routeForWorkflow = ({
 
 const routeForEventHistory = (
   parameters: WorkflowParameters,
-  view?: EventHistoryView,
-): string => {
-  if (!view) {
-    return `${routeForWorkflow(parameters)}/history`;
-  } else {
-    return `${routeForWorkflow(parameters)}/history/${view}`;
-  }
-};
-
-const routeForEventHistoryItem = (
-  parameters: WorkflowParameters,
-  view: EventHistoryView,
-  eventType: EventView,
-  id: string,
-): string => {
-  return `${routeForEventHistory(parameters, view)}/${eventType}-${id}`;
-};
-
-const routeForActivity = (
-  parameters: WorkflowParameters,
-  view: EventHistoryView,
-  eventType: EventView,
-  activityId: string,
-  eventId: string,
+  view?: 'full' | 'json',
 ): string => {
-  return `${routeForEventHistory(
-    parameters,
-    view,
-  )}/${eventType}-${eventId}/events/${activityId}`;
+  if (!view) return `${routeForWorkflow(parameters)}/history`;
+  if (view === 'full') return `${routeForWorkflow(parameters)}/history/summary`;
+  if (view === 'json') return `${routeForWorkflow(parameters)}/history/json`;
 };
 
 const routeForWorkers = (parameters: WorkflowParameters) => {
@@ -101,7 +70,6 @@ export function routeFor(
     | 'workflow'
     | 'workflow.events'
     | 'workflow.events.full'
-    | 'workflow.events.compact'
     | 'workflow.events.json'
     | 'workflow.stack-trace'
     | 'workflow.query'
@@ -109,16 +77,9 @@ export function routeFor(
   parameters: WorkflowParameters,
 ): string;
 export function routeFor(
-  path:
-    | 'workflow.events.full.event'
-    | 'workflow.events.full.pending'
-    | 'workflow.events.compact.activity',
+  path: 'workflow.events.full.event',
   parameters: EventParameter,
 ): string;
-export function routeFor(
-  path: 'workflow.events.compact.activity.event',
-  parameters: ActivityParameter,
-): string;
 export function routeFor(
   path: 'workers',
   parameters: TaskQueueParameter,
@@ -142,51 +103,10 @@ export function routeFor(path: RoutePath, parameters: RouteParameters): string {
     route = routeForEventHistory(parameters, 'full');
   }
 
-  if (path === 'workflow.events.compact') {
-    route = routeForEventHistory(parameters, 'compact');
-  }
-
   if (path === 'workflow.events.json') {
     route = routeForEventHistory(parameters, 'json');
   }
 
-  if (path === 'workflow.events.full.event') {
-    route = routeForEventHistoryItem(
-      parameters,
-      'full',
-      'event',
-      parameters.eventId,
-    );
-  }
-
-  if (path === 'workflow.events.full.pending') {
-    route = routeForEventHistoryItem(
-      parameters,
-      'full',
-      'pending',
-      parameters.eventId,
-    );
-  }
-
-  if (path === 'workflow.events.compact.activity') {
-    route = routeForEventHistoryItem(
-      parameters,
-      'compact',
-      'activity',
-      parameters.eventId,
-    );
-  }
-
-  if (path === 'workflow.events.compact.activity.event') {
-    route = routeForActivity(
-      parameters,
-      'compact',
-      'activity',
-      parameters.eventId,
-      parameters.activityId,
-    );
-  }
-
   if (path === 'workflow.query') {
     route = routeForWorkflow(parameters) + '/query';
   }
diff --git a/src/lib/utilities/update-query-parameters.ts b/src/lib/utilities/update-query-parameters.ts
index 332cea61..184d0a60 100644
--- a/src/lib/utilities/update-query-parameters.ts
+++ b/src/lib/utilities/update-query-parameters.ts
@@ -4,7 +4,7 @@ import { appendQueryParameters } from './append-query-parameters';
 
 type UpdateQueryParams = {
   parameter: string;
-  value?: string | number;
+  value?: string | number | boolean;
   query: URLSearchParams;
   path: string;
   goto: typeof goto;
@@ -25,8 +25,10 @@ export const updateQueryParameters = async ({
   } else {
     query.delete(parameter);
   }
+
   if (browser) {
     goto(appendQueryParameters(path, query), options);
   }
+
   return value;
 };
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
index a023066c..dfd80d0a 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
@@ -8,26 +8,23 @@
     const { workflow: workflowId, run: runId, namespace } = params;
     const parameters = { namespace, executionId: workflowId, runId };
 
-    const events = await fetchEvents(parameters);
+    const { events, eventGroups } = await fetchEvents(parameters);
 
     return {
       props: {
-        workflow: { workflowId, runId, namespace },
+        workflowParameters: { workflowId, runId, namespace },
         events,
       },
       stuff: {
         events,
+        eventGroups,
       },
     };
   };
 </script>
 
 <script lang="ts">
-  import {
-    faCode,
-    faLayerGroup,
-    faStream,
-  } from '@fortawesome/free-solid-svg-icons';
+  import { faCode, faStream } from '@fortawesome/free-solid-svg-icons';
 
   import { routeFor } from '$lib/utilities/route-for';
 
@@ -36,31 +33,29 @@
   import ToggleButtons from '$lib/components/toggle-buttons.svelte';
   import InputAndResults from '$lib/components/input-and-result.svelte';
 
-  export let workflow: WorkflowParameters;
+  export let workflowParameters: WorkflowParameters;
   export let events: HistoryEventWithId[];
 </script>
 
 <section class="flex flex-col gap-4">
   <InputAndResults {events} />
-  <nav class="flex gap-4 justify-between items-end">
-    <h3 class="text-lg font-medium">Event History</h3>
-    <div class="flex gap-4">
-      <ToggleButtons>
-        <ToggleButton
-          icon={faStream}
-          href={routeFor('workflow.events.full', workflow)}
-        />
-        <ToggleButton
-          icon={faLayerGroup}
-          href={routeFor('workflow.events.compact', workflow)}
-        />
-        <ToggleButton
-          icon={faCode}
-          href={routeFor('workflow.events.json', workflow)}
-        />
-      </ToggleButtons>
-      <ExportHistory />
-    </div>
-  </nav>
-  <slot />
+  <section id="event-history">
+    <nav class="flex gap-4 justify-between items-end pb-4">
+      <h3 class="text-lg font-medium">Event History</h3>
+      <div class="flex gap-4">
+        <ToggleButtons>
+          <ToggleButton
+            icon={faStream}
+            href={routeFor('workflow.events.full', workflowParameters)}
+          />
+          <ToggleButton
+            icon={faCode}
+            href={routeFor('workflow.events.json', workflowParameters)}
+          />
+        </ToggleButtons>
+        <ExportHistory />
+      </div>
+    </nav>
+    <slot />
+  </section>
 </section>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
deleted file mode 100644
index 8e19af57..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
+++ /dev/null
@@ -1,26 +0,0 @@
-<script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ stuff }) {
-    const { events } = stuff as { events: HistoryEventWithId[] };
-    const eventGroups = groupEvents(events);
-
-    return {
-      props: { eventGroups },
-      stuff: { eventGroups },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import { groupEvents } from '$lib/models/group-events';
-  import EventTable from '$lib/components/event-table.svelte';
-
-  export let eventGroups: CompactEventGroups;
-</script>
-
-<EventTable events={eventGroups}>
-  <div slot="details" class="w-full h-full py-4">
-    <slot />
-  </div>
-</EventTable>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/__layout.svelte
deleted file mode 100644
index 2c4df3c9..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/__layout.svelte
+++ /dev/null
@@ -1,71 +0,0 @@
-<script context="module" lang="ts">
-  import type { EventParameter } from '$lib/utilities/route-for';
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ params, stuff }) {
-    const { workflow: workflowId, run: runId, namespace, eventId } = params;
-
-    const { eventGroups } = stuff;
-    const group = eventGroups.find(({ id }) => id === eventId);
-
-    if (!group) return { status: 404 };
-
-    const events = [...group.events.values()];
-
-    return {
-      props: {
-        group,
-        events,
-        params: {
-          workflowId,
-          runId,
-          namespace,
-          eventId,
-        },
-      },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import { routeFor } from '$lib/utilities/route-for';
-  import { page } from '$app/stores';
-
-  export let group: CompactEventGroup;
-  export let events: HistoryEventWithId[];
-  export let params: EventParameter;
-
-  const getHref = (
-    group: CompactEventGroup,
-    event: HistoryEventWithId,
-  ): string =>
-    routeFor('workflow.events.compact.activity.event', {
-      ...params,
-      activityId: group.id,
-      eventId: event.id,
-    });
-</script>
-
-<div class="flex flex-col w-full h-full">
-  <nav class="mb-4">
-    <ul class="flex gap-4 w-full items-start">
-      {#each events as event}
-        <li>
-          <a
-            href={getHref(group, event)}
-            class:active={$page.url.pathname === getHref(group, event)}
-          >
-            {event.eventType}
-          </a>
-        </li>
-      {/each}
-    </ul>
-  </nav>
-  <slot />
-</div>
-
-<style lang="postcss">
-  .active {
-    @apply text-blue-700 border-b-2 border-blue-600;
-  }
-</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/events/[id].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/events/[id].svelte
deleted file mode 100644
index a1ff6411..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/events/[id].svelte
+++ /dev/null
@@ -1,27 +0,0 @@
-<script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ params, stuff }) {
-    const { events } = stuff;
-
-    const event: HistoryEventWithId = events.find(
-      (event: HistoryEventWithId) => event.id === params.id,
-    );
-
-    if (!event) {
-      return { status: 404 };
-    }
-
-    return {
-      props: { attributes: event.attributes },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import EventDetails from '$lib/components/event-details.svelte';
-
-  export let attributes: EventAttribute;
-</script>
-
-<EventDetails {attributes} />
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/index.svelte
deleted file mode 100644
index 3e409081..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/index.svelte
+++ /dev/null
@@ -1,19 +0,0 @@
-<script context="module" lang="ts">
-  import { routeFor } from '$lib/utilities/route-for';
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ params }) {
-    const { workflow: workflowId, run: runId, namespace, eventId } = params;
-
-    return {
-      status: 302,
-      redirect: routeFor('workflow.events.compact.activity.event', {
-        namespace,
-        workflowId,
-        runId,
-        eventId,
-        activityId: eventId,
-      }),
-    };
-  };
-</script>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/index.svelte
deleted file mode 100644
index 12dbabb1..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/index.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="flex items-center justify-center w-full h-full">
-  <p>Select an item from the left.</p>
-</div>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte
deleted file mode 100644
index 64a7f9af..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte
+++ /dev/null
@@ -1,43 +0,0 @@
-<script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ url, stuff }) {
-    const { workflow, events } = stuff;
-    const category = url.searchParams.get('category');
-
-    return {
-      props: { workflow, events, category },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import { getVisibleEvents } from '$lib/utilities/get-visible-events';
-
-  import FilterSelect from '$lib/components/select/filter-select.svelte';
-  import Option from '$lib/components/select/option.svelte';
-  import EventTable from '$lib/components/event-table.svelte';
-
-  export let workflow: WorkflowExecution;
-  export let events: HistoryEventWithId[];
-  export let category: EventTypeCategory = null;
-
-  $: visibleEvents = getVisibleEvents(events, workflow, category);
-</script>
-
-<EventTable events={visibleEvents}>
-  <div slot="filters">
-    <FilterSelect parameter="category" bind:value={category}>
-      <Option value={undefined}>All</Option>
-      <Option value="activity">Activity</Option>
-      <Option value="command">Command</Option>
-      <Option value="signal">Signal</Option>
-      <Option value="timer">Timer</Option>
-      <Option value="child-workflow">Child Workflow</Option>
-      <Option value="workflow">Workflow</Option>
-    </FilterSelect>
-  </div>
-  <div slot="details" class="w-full h-full py-4">
-    <slot />
-  </div>
-</EventTable>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/event-[id].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/event-[id].svelte
deleted file mode 100644
index e563488f..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/event-[id].svelte
+++ /dev/null
@@ -1,28 +0,0 @@
-<script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ params, stuff }) {
-    const { events } = stuff;
-    const { id } = params;
-
-    const event: HistoryEventWithId = events.find(
-      (event: HistoryEventWithId) => event.id === id,
-    );
-
-    if (!event) {
-      return { status: 404 };
-    }
-
-    return {
-      props: { attributes: event.attributes },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import EventDetails from '$lib/components/event-details.svelte';
-
-  export let attributes: EventAttribute;
-</script>
-
-<EventDetails {attributes} />
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/index.svelte
deleted file mode 100644
index 12dbabb1..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/index.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="flex items-center justify-center w-full h-full">
-  <p>Select an item from the left.</p>
-</div>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/pending-[id].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/pending-[id].svelte
deleted file mode 100644
index 024eb455..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/pending-[id].svelte
+++ /dev/null
@@ -1,30 +0,0 @@
-<script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ stuff }) {
-    const { workflow, events } = stuff;
-
-    return {
-      props: { workflow, events },
-    };
-  };
-</script>
-
-<script lang="ts">
-  import { page } from '$app/stores';
-  import EventDetails from '$lib/components/event-details.svelte';
-
-  const findActivity = (
-    workflow: WorkflowExecution,
-    id: string,
-  ): PendingActivity => {
-    return workflow.pendingActivities.find((activity) => activity.id === id);
-  };
-
-  export let workflow: WorkflowExecution;
-  const pendingActivity = findActivity(workflow, $page.params.id);
-</script>
-
-{#await pendingActivity then activity}
-  <EventDetails attributes={activity} />
-{/await}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte
index 7aeb6813..f1e4931e 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte
@@ -2,6 +2,9 @@
   import type { Load } from '@sveltejs/kit';
 
   export const load: Load = async function ({ url }) {
-    return { status: 302, redirect: `${url.pathname}/full` };
+    return {
+      status: 302,
+      redirect: `${url.pathname}/summary`,
+    };
   };
 </script>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte
new file mode 100644
index 00000000..e02d8916
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte
@@ -0,0 +1,59 @@
+<script lang="ts" context="module">
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ params, stuff }) {
+    const { eventId } = params;
+    const { events, eventGroups } = stuff;
+
+    const event: HistoryEventWithId = events.find(
+      (event: HistoryEventWithId) => event.id === eventId,
+    );
+
+    const eventGroup: CompactEventGroup = getGroupForEvent(event, eventGroups);
+
+    if (!event) {
+      return { status: 404 };
+    }
+
+    return {
+      props: { attributes: event.attributes, eventGroup, eventId },
+    };
+  };
+</script>
+
+<script lang="ts">
+  import EventDetails from '$lib/components/event-details.svelte';
+  import { getGroupForEvent } from '$lib/models/group-events';
+
+  export let attributes: EventAttribute;
+  export let eventGroup: CompactEventGroup;
+  export let eventId: string;
+</script>
+
+<section class="overflow-y-scroll max-h-full">
+  {#if eventGroup}
+    <nav class="flex flex-col mb-4">
+      <ul class="flex gap-4 w-full items-start">
+        {#each [...eventGroup.events] as [id, event]}
+          <li>
+            <a
+              sveltekit:noscroll
+              href={id}
+              class:active={id === eventId}
+              class="border-b-2 border-blue-600"
+            >
+              {event.eventType}
+            </a>
+          </li>
+        {/each}
+      </ul>
+    </nav>
+  {/if}
+  <EventDetails {attributes} />
+</section>
+
+<style lang="postcss">
+  .active {
+    @apply text-blue-700 border-b-2 border-blue-600;
+  }
+</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte
new file mode 100644
index 00000000..e8dd922e
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte
@@ -0,0 +1,95 @@
+<script context="module" lang="ts">
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ stuff, url }) {
+    const { events, eventGroups } = stuff;
+
+    const category = url.searchParams.get('category');
+    const compact = !!url.searchParams.get('compact');
+
+    return {
+      props: {
+        events,
+        eventGroups,
+        category,
+        compact,
+      },
+    };
+  };
+</script>
+
+<script lang="ts">
+  import EmptyState from '$lib/components/empty-state.svelte';
+  import Event from '$lib/components/event.svelte';
+  import FilterCheckbox from '$lib/components/filter-checkbox.svelte';
+  import FilterSelect from '$lib/components/select/filter-select.svelte';
+  import Option from '$lib/components/select/option.svelte';
+
+  export let events: HistoryEventWithId[];
+  export let eventGroups: CompactEventGroups;
+
+  export let compact = false;
+  export let category: EventTypeCategory = null;
+
+  $: eventsForView = compact ? eventGroups : events;
+  $: visibleEvents = [...eventsForView].filter(
+    (event: HistoryEventWithId | CompactEventGroup) =>
+      !category || event.category === category,
+  );
+</script>
+
+<section class="flex flex-col border-2 border-gray-300 rounded-lg w-full mb-6">
+  <div class="flex w-full">
+    <header class="table-header border-r-2 rounded-tl-lg w-1/3">
+      <h3>Summary</h3>
+      <div class="flex items-center gap-4">
+        <FilterSelect parameter="category" bind:value={category}>
+          <Option value={undefined}>All</Option>
+          <Option value="activity">Activity</Option>
+          <Option value="command">Command</Option>
+          <Option value="signal">Signal</Option>
+          <Option value="timer">Timer</Option>
+          <Option value="child-workflow">Child Workflow</Option>
+          <Option value="workflow">Workflow</Option>
+        </FilterSelect>
+        <FilterCheckbox
+          parameter="compact"
+          name="Compact"
+          bind:value={compact}
+        />
+      </div>
+    </header>
+    <header class="table-header rounded-tr-lg w-2/3">
+      <h3>Details</h3>
+    </header>
+  </div>
+  <div class="flex">
+    <div class="flex flex-col w-1/3 border-r-2 border-gray-300 rounded-bl-lg">
+      <div class="rounded-bl-lg overflow-y-scroll">
+        {#each visibleEvents as event (event.id)}
+          {#if !category || event.category === category}<Event {event} />{/if}
+        {:else}
+          <div class="p-2">
+            <EmptyState
+              title="No Events Match"
+              content="There are know events that match your filters. Adjust your filters to see your events."
+            />
+          </div>
+        {/each}
+      </div>
+    </div>
+    <div class="flex flex-col w-2/3">
+      <div
+        class="overflow-y-scroll overflow-x-hidden rounded-br-lg px-4 w-full py-4"
+      >
+        <slot />
+      </div>
+    </div>
+  </div>
+</section>
+
+<style lang="postcss">
+  .table-header {
+    @apply bg-gray-100 text-gray-800 font-semibold p-4 flex justify-between items-center border-b-2;
+  }
+</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte
new file mode 100644
index 00000000..5f157973
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte
@@ -0,0 +1,12 @@
+<script context="module" lang="ts">
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ url, stuff }) {
+    const [firstEvent] = stuff.events;
+
+    return {
+      status: 302,
+      redirect: `${url.pathname}/${firstEvent.id}`,
+    };
+  };
+</script>


Test patch:
diff --git a/src/lib/models/event-history/get-event-classification.test.ts b/src/lib/models/event-history/get-event-classification.test.ts
new file mode 100644
index 00000000..0b5aae6f
--- /dev/null
+++ b/src/lib/models/event-history/get-event-classification.test.ts
@@ -0,0 +1,231 @@
+import { getEventClassification } from './get-event-classification';
+
+describe(getEventClassification, () => {
+  it('should return "Started" for WorkflowExecutionStartedEvent', () => {
+    expect(getEventClassification('WorkflowExecutionStartedEvent')).toBe(
+      'Started',
+    );
+  });
+
+  it('should return "Completed" for WorkflowExecutionCompletedEvent', () => {
+    expect(getEventClassification('WorkflowExecutionCompletedEvent')).toBe(
+      'Completed',
+    );
+  });
+
+  it('should return "Failed" for WorkflowExecutionFailedEvent', () => {
+    expect(getEventClassification('WorkflowExecutionFailedEvent')).toBe(
+      'Failed',
+    );
+  });
+
+  it('should return "TimedOut" for WorkflowExecutionTimedOutEvent', () => {
+    expect(getEventClassification('WorkflowExecutionTimedOutEvent')).toBe(
+      'TimedOut',
+    );
+  });
+
+  it('should return "Scheduled" for WorkflowTaskScheduledEvent', () => {
+    expect(getEventClassification('WorkflowTaskScheduledEvent')).toBe(
+      'Scheduled',
+    );
+  });
+
+  it('should return "Started" for WorkflowTaskStartedEvent', () => {
+    expect(getEventClassification('WorkflowTaskStartedEvent')).toBe('Started');
+  });
+
+  it('should return "Completed" for WorkflowTaskCompletedEvent', () => {
+    expect(getEventClassification('WorkflowTaskCompletedEvent')).toBe(
+      'Completed',
+    );
+  });
+
+  it('should return "TimedOut" for WorkflowTaskTimedOutEvent', () => {
+    expect(getEventClassification('WorkflowTaskTimedOutEvent')).toBe(
+      'TimedOut',
+    );
+  });
+
+  it('should return "Failed" for WorkflowTaskFailedEvent', () => {
+    expect(getEventClassification('WorkflowTaskFailedEvent')).toBe('Failed');
+  });
+
+  it('should return "Scheduled" for ActivityTaskScheduledEvent', () => {
+    expect(getEventClassification('ActivityTaskScheduledEvent')).toBe(
+      'Scheduled',
+    );
+  });
+
+  it('should return "Started" for ActivityTaskStartedEvent', () => {
+    expect(getEventClassification('ActivityTaskStartedEvent')).toBe('Started');
+  });
+
+  it('should return "Completed" for ActivityTaskCompletedEvent', () => {
+    expect(getEventClassification('ActivityTaskCompletedEvent')).toBe(
+      'Completed',
+    );
+  });
+
+  it('should return "Failed" for ActivityTaskFailedEvent', () => {
+    expect(getEventClassification('ActivityTaskFailedEvent')).toBe('Failed');
+  });
+
+  it('should return "TimedOut" for ActivityTaskTimedOutEvent', () => {
+    expect(getEventClassification('ActivityTaskTimedOutEvent')).toBe(
+      'TimedOut',
+    );
+  });
+
+  it('should return "Started" for TimerStartedEven', () => {
+    expect(getEventClassification('TimerStartedEvent')).toBe('Started');
+  });
+
+  it('should return "Fired" for TimerFiredEven', () => {
+    expect(getEventClassification('TimerFiredEvent')).toBe('Fired');
+  });
+
+  it('should return "CancelRequested" for ActivityTaskCancelRequestedEvent', () => {
+    expect(getEventClassification('ActivityTaskCancelRequestedEvent')).toBe(
+      'CancelRequested',
+    );
+  });
+
+  it('should return "Canceled" for ActivityTaskCanceledEvent', () => {
+    expect(getEventClassification('ActivityTaskCanceledEvent')).toBe(
+      'Canceled',
+    );
+  });
+
+  it('should return "Canceled" for TimerCanceledEvent', () => {
+    expect(getEventClassification('TimerCanceledEventt')).toBe('Canceled');
+  });
+
+  it('should return undefined for MarkerRecordedEvent', () => {
+    expect(getEventClassification('MarkerRecordedEvent')).toBeUndefined();
+  });
+
+  it('should return "Signaled" for WorkflowExecutionSignaledEvent', () => {
+    expect(getEventClassification('WorkflowExecutionSignaledEvent')).toBe(
+      'Signaled',
+    );
+  });
+
+  it('should return "Terminated" for WorkflowExecutionTerminatedEvent', () => {
+    expect(getEventClassification('WorkflowExecutionTerminatedEvent')).toBe(
+      'Terminated',
+    );
+  });
+
+  it('should return "CancelRequested" for WorkflowExecutionCancelRequestedEvent', () => {
+    expect(
+      getEventClassification('WorkflowExecutionCancelRequestedEvent'),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "Canceled" for WorkflowExecutionCanceledEvent', () => {
+    expect(getEventClassification('WorkflowExecutionCanceledEvent')).toBe(
+      'Canceled',
+    );
+  });
+
+  it('should return "$x" for RequestCancelExternalWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification(
+        'RequestCancelExternalWorkflowExecutionInitiatedEvent',
+      ),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "$x" for RequestCancelExternalWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification(
+        'RequestCancelExternalWorkflowExecutionFailedEvent',
+      ),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "CancelRequested" for ExternalWorkflowExecutionCancelRequestedEvent', () => {
+    expect(
+      getEventClassification('ExternalWorkflowExecutionCancelRequestedEvent'),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "New" for WorkflowExecutionContinuedAsNewEvent', () => {
+    expect(getEventClassification('WorkflowExecutionContinuedAsNewEvent')).toBe(
+      'New',
+    );
+  });
+
+  it('should return "$x" for StartChildWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification('StartChildWorkflowExecutionInitiatedEvent'),
+    ).toBe('Initiated');
+  });
+
+  it('should return "Failed" for StartChildWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification('StartChildWorkflowExecutionFailedEvent'),
+    ).toBe('Failed');
+  });
+
+  it('should return "Started" for ChildWorkflowExecutionStartedEvent', () => {
+    expect(getEventClassification('ChildWorkflowExecutionStartedEvent')).toBe(
+      'Started',
+    );
+  });
+
+  it('should return "Completed" for ChildWorkflowExecutionCompletedEvent', () => {
+    expect(getEventClassification('ChildWorkflowExecutionCompletedEvent')).toBe(
+      'Completed',
+    );
+  });
+
+  it('should return "Failed" for ChildWorkflowExecutionFailedEvent', () => {
+    expect(getEventClassification('ChildWorkflowExecutionFailedEvent')).toBe(
+      'Failed',
+    );
+  });
+
+  it('should return "Canceled" for ChildWorkflowExecutionCanceledEvent', () => {
+    expect(getEventClassification('ChildWorkflowExecutionCanceledEvent')).toBe(
+      'Canceled',
+    );
+  });
+
+  it('should return "TimedOut" for ChildWorkflowExecutionTimedOutEvent', () => {
+    expect(getEventClassification('ChildWorkflowExecutionTimedOutEvent')).toBe(
+      'TimedOut',
+    );
+  });
+
+  it('should return "Terminated" for ChildWorkflowExecutionTerminatedEvent', () => {
+    expect(
+      getEventClassification('ChildWorkflowExecutionTerminatedEvent'),
+    ).toBe('Terminated');
+  });
+
+  it('should return "Initiated" for SignalExternalWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification('SignalExternalWorkflowExecutionInitiatedEvent'),
+    ).toBe('Initiated');
+  });
+
+  it('should return "Failed" for SignalExternalWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification('SignalExternalWorkflowExecutionFailedEvent'),
+    ).toBe('Failed');
+  });
+
+  it('should return "Signaled" for ExternalWorkflowExecutionSignaledEvent', () => {
+    expect(
+      getEventClassification('ExternalWorkflowExecutionSignaledEvent'),
+    ).toBe('Signaled');
+  });
+
+  it('should return undefined for UpsertWorkflowSearchAttributesEvent', () => {
+    expect(
+      getEventClassification('UpsertWorkflowSearchAttributesEvent'),
+    ).toBeUndefined();
+  });
+});
diff --git a/src/lib/models/event-history/is-event.test.ts b/src/lib/models/event-history/is-event.test.ts
new file mode 100644
index 00000000..c2f2d3f8
--- /dev/null
+++ b/src/lib/models/event-history/is-event.test.ts
@@ -0,0 +1,31 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { isEvent } from '.';
+
+describe(isEvent, () => {
+  it('should return true if the event has an eventType', () => {
+    expect(isEvent({ eventType: 'Workflow Started' })).toBe(true);
+  });
+
+  it('should return false if the event does not have an eventType', () => {
+    expect(isEvent({ activityType: 'Workflow Started' })).toBe(false);
+  });
+
+  it('should return false if passed null', () => {
+    expect(isEvent(null)).toBe(false);
+  });
+
+  it('should return false if passed a string', () => {
+    expect(isEvent('string')).toBe(false);
+  });
+
+  it('should return false if passed a number', () => {
+    expect(isEvent(4)).toBe(false);
+  });
+
+  it('should return false if passed an array', () => {
+    expect(isEvent([])).toBe(false);
+  });
+});
diff --git a/src/lib/models/group-events/create-event-group.test.ts b/src/lib/models/group-events/create-event-group.test.ts
index 1aea4756..a7b06f4a 100644
--- a/src/lib/models/group-events/create-event-group.test.ts
+++ b/src/lib/models/group-events/create-event-group.test.ts
@@ -96,7 +96,7 @@ describe(createEventGroup, () => {
   it('should store the groupTaskScheduled', () => {
     const group = createEventGroup(scheduledEvent);
 
-    expect(group.events.get('ActivityTaskScheduled')).toBe(scheduledEvent);
+    expect(group.events.get(scheduledEvent.id)).toBe(scheduledEvent);
   });
 
   it('should be able to add a started event', () => {
diff --git a/src/lib/models/group-events/get-group-for-event.test.ts b/src/lib/models/group-events/get-group-for-event.test.ts
new file mode 100644
index 00000000..dd06960c
--- /dev/null
+++ b/src/lib/models/group-events/get-group-for-event.test.ts
@@ -0,0 +1,113 @@
+import { groupEvents } from '.';
+import { getGroupForEvent } from './get-group-for-event';
+
+const eventHistory = [
+  {
+    id: '5',
+    eventId: '5',
+    eventTime: '2022-03-10T16:47:09.220652296Z',
+    eventType: 'ActivityTaskScheduled',
+    version: '0',
+    taskId: '1048699',
+    activityTaskScheduledEventAttributes: {
+      activityId: '5',
+      activityType: {
+        name: 'CompletedActivity',
+      },
+      namespace: '',
+      taskQueue: {
+        name: 'rainbow-statuses',
+        kind: 'Normal',
+      },
+      header: {
+        fields: {},
+      },
+      input: null,
+      scheduleToCloseTimeout: '0s',
+      scheduleToStartTimeout: '0s',
+      startToCloseTimeout: '3600s',
+
+      id: '4',
+      heartbeatTimeout: '0s',
+      workflowTaskCompletedEventId: '4',
+      retryPolicy: {
+        initialInterval: '1s',
+        backoffCoefficient: 2,
+        maximumInterval: '100s',
+        maximumAttempts: 1,
+        nonRetryableErrorTypes: [],
+      },
+    },
+  },
+  {
+    eventId: '6',
+    eventTime: '2022-03-10T16:47:09.226184379Z',
+    eventType: 'ActivityTaskStarted',
+    version: '0',
+    taskId: '1048703',
+    activityTaskStartedEventAttributes: {
+      scheduledEventId: '5',
+      identity: '14327@MacBook-Pro.local@',
+      requestId: '772583b9-1cb4-41b7-8507-b4214d1bf972',
+      attempt: 1,
+      lastFailure: null,
+    },
+  },
+  {
+    id: '7',
+    eventId: '7',
+    eventTime: '2022-03-10T16:47:09.231417213Z',
+    eventType: 'ActivityTaskCompleted',
+    version: '0',
+    taskId: '1048704',
+    activityTaskCompletedEventAttributes: {
+      result: null,
+      scheduledEventId: '5',
+      startedEventId: '6',
+      identity: '14327@MacBook-Pro.local@',
+    },
+  },
+];
+
+const [activityTaskScheduled, activityTaskStarted, activityTaskCompleted] =
+  eventHistory;
+const eventGroups = groupEvents(eventHistory);
+const [eventGroup] = eventGroups;
+
+describe(getGroupForEvent, () => {
+  it('should get ActivityScheduledEvent', () => {
+    const result = getGroupForEvent(activityTaskScheduled, eventGroups);
+    expect(result).toBe(eventGroup);
+  });
+
+  it('should get ActivityStartedEvent', () => {
+    const result = getGroupForEvent(activityTaskStarted, eventGroups);
+    expect(result).toBe(eventGroup);
+  });
+
+  it('should get ActivityCompletedEvent', () => {
+    const result = getGroupForEvent(activityTaskCompleted, eventGroups);
+    expect(result).toBe(eventGroup);
+  });
+
+  it('should return undefined if an event is not in a group', () => {
+    const anotherEvent = {
+      id: '8',
+      eventId: '8',
+      eventTime: '2022-03-10T16:47:09.231417213Z',
+      eventType: 'ActivityTaskCompleted',
+      version: '0',
+      taskId: '1048704',
+      activityTaskCompletedEventAttributes: {
+        result: null,
+        scheduledEventId: '1',
+        startedEventId: '2',
+        identity: '14327@MacBook-Pro.local@',
+      },
+    };
+
+    const result = getGroupForEvent(anotherEvent, eventGroups);
+
+    expect(result).toBe(undefined);
+  });
+});
diff --git a/src/lib/models/group-events/group-events.test.ts b/src/lib/models/group-events/group-events.test.ts
index 5ca44eb9..dd37654f 100644
--- a/src/lib/models/group-events/group-events.test.ts
+++ b/src/lib/models/group-events/group-events.test.ts
@@ -85,7 +85,7 @@ describe(groupEvents, () => {
     const groups = groupEvents([scheduledEvent]);
     const group = groups.find(({ id }) => id === scheduledEvent.id);
 
-    expect(group.events.get('ActivityTaskScheduled')).toBe(scheduledEvent);
+    expect(group.events.get(scheduledEvent.id)).toBe(scheduledEvent);
   });
 
   it('should be able to store multiple event groups', () => {
@@ -100,7 +100,7 @@ describe(groupEvents, () => {
     const group = groups.find(({ id }) => id === scheduledEvent.id);
 
     expect(group.events.size).toBe(2);
-    expect(group.events.get('ActivityTaskCompleted')).toBe(completedEvent);
+    expect(group.events.get(completedEvent.id)).toBe(completedEvent);
   });
 
   it('should be able to add multiple event groups and their associated events', () => {
diff --git a/src/lib/utilities/get-event-classification.test.ts b/src/lib/utilities/get-event-classification.test.ts
deleted file mode 100644
index a9ad295c..00000000
--- a/src/lib/utilities/get-event-classification.test.ts
+++ /dev/null
@@ -1,361 +0,0 @@
-import {
-  getEventClassification,
-  isEvent,
-  isPendingActivity,
-  formatEvent,
-} from './get-event-classification';
-
-describe(getEventClassification, () => {
-  it('should return "Started" for WorkflowExecutionStartedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionStartedEvent' }),
-    ).toBe('Started');
-  });
-
-  it('should return "Completed" for WorkflowExecutionCompletedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionCompletedEvent' }),
-    ).toBe('Completed');
-  });
-
-  it('should return "Failed" for WorkflowExecutionFailedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionFailedEvent' }),
-    ).toBe('Failed');
-  });
-
-  it('should return "TimedOut" for WorkflowExecutionTimedOutEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionTimedOutEvent' }),
-    ).toBe('TimedOut');
-  });
-
-  it('should return "Scheduled" for WorkflowTaskScheduledEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowTaskScheduledEvent' }),
-    ).toBe('Scheduled');
-  });
-
-  it('should return "Started" for WorkflowTaskStartedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowTaskStartedEvent' }),
-    ).toBe('Started');
-  });
-
-  it('should return "Completed" for WorkflowTaskCompletedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowTaskCompletedEvent' }),
-    ).toBe('Completed');
-  });
-
-  it('should return "TimedOut" for WorkflowTaskTimedOutEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowTaskTimedOutEvent' }),
-    ).toBe('TimedOut');
-  });
-
-  it('should return "Failed" for WorkflowTaskFailedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowTaskFailedEvent' }),
-    ).toBe('Failed');
-  });
-
-  it('should return "Scheduled" for ActivityTaskScheduledEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskScheduledEvent' }),
-    ).toBe('Scheduled');
-  });
-
-  it('should return "Started" for ActivityTaskStartedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskStartedEvent' }),
-    ).toBe('Started');
-  });
-
-  it('should return "Completed" for ActivityTaskCompletedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskCompletedEvent' }),
-    ).toBe('Completed');
-  });
-
-  it('should return "Failed" for ActivityTaskFailedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskFailedEvent' }),
-    ).toBe('Failed');
-  });
-
-  it('should return "TimedOut" for ActivityTaskTimedOutEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskTimedOutEvent' }),
-    ).toBe('TimedOut');
-  });
-
-  it('should return "Started" for TimerStartedEven', () => {
-    expect(getEventClassification({ eventType: 'TimerStartedEven' })).toBe(
-      'Started',
-    );
-  });
-
-  it('should return "Fired" for TimerFiredEven', () => {
-    expect(getEventClassification({ eventType: 'TimerFiredEven' })).toBe(
-      'Fired',
-    );
-  });
-
-  it('should return "CancelRequested" for ActivityTaskCancelRequestedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskCancelRequestedEvent' }),
-    ).toBe('CancelRequested');
-  });
-
-  it('should return "Canceled" for ActivityTaskCanceledEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'ActivityTaskCanceledEvent' }),
-    ).toBe('Canceled');
-  });
-
-  it('should return "Canceled" for TimerCanceledEvent', () => {
-    expect(getEventClassification({ eventType: 'TimerCanceledEvent' })).toBe(
-      'Canceled',
-    );
-  });
-
-  it('should return undefined for MarkerRecordedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'MarkerRecordedEvent' }),
-    ).toBeUndefined();
-  });
-
-  it('should return "Signaled" for WorkflowExecutionSignaledEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionSignaledEvent' }),
-    ).toBe('Signaled');
-  });
-
-  it('should return "Terminated" for WorkflowExecutionTerminatedEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionTerminatedEvent' }),
-    ).toBe('Terminated');
-  });
-
-  it('should return "CancelRequested" for WorkflowExecutionCancelRequestedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'WorkflowExecutionCancelRequestedEvent',
-      }),
-    ).toBe('CancelRequested');
-  });
-
-  it('should return "Canceled" for WorkflowExecutionCanceledEvent', () => {
-    expect(
-      getEventClassification({ eventType: 'WorkflowExecutionCanceledEvent' }),
-    ).toBe('Canceled');
-  });
-
-  it('should return "$x" for RequestCancelExternalWorkflowExecutionInitiatedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'RequestCancelExternalWorkflowExecutionInitiatedEvent',
-      }),
-    ).toBe('CancelRequested');
-  });
-
-  it('should return "$x" for RequestCancelExternalWorkflowExecutionFailedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'RequestCancelExternalWorkflowExecutionFailedEvent',
-      }),
-    ).toBe('CancelRequested');
-  });
-
-  it('should return "CancelRequested" for ExternalWorkflowExecutionCancelRequestedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ExternalWorkflowExecutionCancelRequestedEvent',
-      }),
-    ).toBe('CancelRequested');
-  });
-
-  it('should return "New" for WorkflowExecutionContinuedAsNewEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'WorkflowExecutionContinuedAsNewEvent',
-      }),
-    ).toBe('New');
-  });
-
-  it('should return "$x" for StartChildWorkflowExecutionInitiatedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'StartChildWorkflowExecutionInitiatedEvent',
-      }),
-    ).toBe('Initiated');
-  });
-
-  it('should return "Failed" for StartChildWorkflowExecutionFailedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'StartChildWorkflowExecutionFailedEvent',
-      }),
-    ).toBe('Failed');
-  });
-
-  it('should return "Started" for ChildWorkflowExecutionStartedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionStartedEvent',
-      }),
-    ).toBe('Started');
-  });
-
-  it('should return "Completed" for ChildWorkflowExecutionCompletedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionCompletedEvent',
-      }),
-    ).toBe('Completed');
-  });
-
-  it('should return "Failed" for ChildWorkflowExecutionFailedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionFailedEvent',
-      }),
-    ).toBe('Failed');
-  });
-
-  it('should return "Canceled" for ChildWorkflowExecutionCanceledEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionCanceledEvent',
-      }),
-    ).toBe('Canceled');
-  });
-
-  it('should return "TimedOut" for ChildWorkflowExecutionTimedOutEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionTimedOutEvent',
-      }),
-    ).toBe('TimedOut');
-  });
-
-  it('should return "Terminated" for ChildWorkflowExecutionTerminatedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ChildWorkflowExecutionTerminatedEvent',
-      }),
-    ).toBe('Terminated');
-  });
-
-  it('should return "Initiated" for SignalExternalWorkflowExecutionInitiatedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'SignalExternalWorkflowExecutionInitiatedEvent',
-      }),
-    ).toBe('Initiated');
-  });
-
-  it('should return "Failed" for SignalExternalWorkflowExecutionFailedEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'SignalExternalWorkflowExecutionFailedEvent',
-      }),
-    ).toBe('Failed');
-  });
-
-  it('should return "Signaled" for ExternalWorkflowExecutionSignaledEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'ExternalWorkflowExecutionSignaledEvent',
-      }),
-    ).toBe('Signaled');
-  });
-
-  it('should return undefined for UpsertWorkflowSearchAttributesEvent', () => {
-    expect(
-      getEventClassification({
-        eventType: 'UpsertWorkflowSearchAttributesEvent',
-      }),
-    ).toBeUndefined();
-  });
-});
-
-describe(isEvent, () => {
-  it('should return true if the event has an eventType', () => {
-    expect(isEvent({ eventType: 'Workflow Started' })).toBe(true);
-  });
-
-  it('should return false if the event does not have an eventType', () => {
-    expect(isEvent({ activityType: 'Workflow Started' })).toBe(false);
-  });
-
-  it('should return false if passed null', () => {
-    expect(isEvent(null)).toBe(false);
-  });
-
-  it('should return false if passed a string', () => {
-    expect(isEvent('string')).toBe(false);
-  });
-
-  it('should return false if passed a number', () => {
-    expect(isEvent(4)).toBe(false);
-  });
-
-  it('should return false if passed an array', () => {
-    expect(isEvent([])).toBe(false);
-  });
-});
-
-describe(isPendingActivity, () => {
-  it('should return true if the event has an eventType', () => {
-    expect(
-      isPendingActivity({ activityType: { name: 'Workflow Started' } }),
-    ).toBe(true);
-  });
-
-  it('should return false if the event does not have an eventType', () => {
-    expect(isPendingActivity({ eventType: 'Workflow Started' })).toBe(false);
-  });
-
-  it('should return false if passed null', () => {
-    expect(isPendingActivity(null)).toBe(false);
-  });
-
-  it('should return false if passed a string', () => {
-    expect(isPendingActivity('string')).toBe(false);
-  });
-
-  it('should return false if passed a number', () => {
-    expect(isPendingActivity(4)).toBe(false);
-  });
-
-  it('should return false if passed an array', () => {
-    expect(isPendingActivity([])).toBe(false);
-  });
-});
-
-describe(formatEvent, () => {
-  it('should correctly get the name from an event', () => {
-    const result = formatEvent({ eventType: 'WorkflowStarted' });
-    expect(result.name).toBe('Workflow Started');
-  });
-
-  it('should correctly get the tag from an event', () => {
-    const result = formatEvent({ eventType: 'WorkflowStarted' });
-    expect(result.tag).toBe('WorkflowStarted');
-  });
-
-  it('should correctly get the name from an activity', () => {
-    const result = formatEvent({
-      activityType: { name: 'LongActivity' },
-      state: 'Started',
-    });
-    expect(result.name).toBe('Long Activity: Started');
-  });
-
-  it('should correctly get the tag from an activity', () => {
-    const result = formatEvent({ eventType: 'WorkflowStarted' });
-    expect(result.tag).toBe('WorkflowStarted');
-  });
-});
diff --git a/src/lib/utilities/is-pending-activity.test.ts b/src/lib/utilities/is-pending-activity.test.ts
new file mode 100644
index 00000000..072ccd60
--- /dev/null
+++ b/src/lib/utilities/is-pending-activity.test.ts
@@ -0,0 +1,29 @@
+import { isPendingActivity } from './is-pending-activity';
+
+describe(isPendingActivity, () => {
+  it('should return true if the event has an activityType', () => {
+    expect(
+      isPendingActivity({ activityType: { name: 'Workflow Started' } }),
+    ).toBe(true);
+  });
+
+  it('should return false if the event does not have an activityType', () => {
+    expect(isPendingActivity({ eventType: 'Workflow Started' })).toBe(false);
+  });
+
+  it('should return false if passed null', () => {
+    expect(isPendingActivity(null)).toBe(false);
+  });
+
+  it('should return false if passed a string', () => {
+    expect(isPendingActivity('string')).toBe(false);
+  });
+
+  it('should return false if passed a number', () => {
+    expect(isPendingActivity(4)).toBe(false);
+  });
+
+  it('should return false if passed an array', () => {
+    expect(isPendingActivity([])).toBe(false);
+  });
+});
diff --git a/src/lib/utilities/route-for.test.ts b/src/lib/utilities/route-for.test.ts
index 8e8ac432..64ae5b51 100644
--- a/src/lib/utilities/route-for.test.ts
+++ b/src/lib/utilities/route-for.test.ts
@@ -30,52 +30,7 @@ describe(routeFor, () => {
       workflowId: 'abc',
       runId: 'def',
     });
-    expect(path).toBe('/namespaces/default/workflows/abc/def/history/full');
-  });
-
-  it('should route to "workflow".events.full.event', () => {
-    const path = routeFor('workflow.events.full.event', {
-      namespace: 'default',
-      workflowId: 'abc',
-      runId: 'def',
-      eventId: '1',
-    });
-    expect(path).toBe(
-      '/namespaces/default/workflows/abc/def/history/full/event-1',
-    );
-  });
-
-  it('should route to "workflow".events.full.pending', () => {
-    const path = routeFor('workflow.events.full.pending', {
-      namespace: 'default',
-      workflowId: 'abc',
-      runId: 'def',
-      eventId: '1',
-    });
-    expect(path).toBe(
-      '/namespaces/default/workflows/abc/def/history/full/pending-1',
-    );
-  });
-
-  it('should route to "workflow".events.compact', () => {
-    const path = routeFor('workflow.events.compact', {
-      namespace: 'default',
-      workflowId: 'abc',
-      runId: 'def',
-    });
-    expect(path).toBe('/namespaces/default/workflows/abc/def/history/compact');
-  });
-
-  it('should route to "workflow".events.compact.activity', () => {
-    const path = routeFor('workflow.events.compact.activity', {
-      namespace: 'default',
-      workflowId: 'abc',
-      runId: 'def',
-      eventId: '1',
-    });
-    expect(path).toBe(
-      '/namespaces/default/workflows/abc/def/history/compact/activity-1',
-    );
+    expect(path).toBe('/namespaces/default/workflows/abc/def/history/summary');
   });
 
   it('should route to "workflow".events.json', () => {