Repository: temporalio/ui
Pull request #181 (2021-12-23 18:47:31)

Update workflow header, implement new event history designs
- Updates font to Poppins
- Implements tabs and their associated routing
- Removes margins in favor of Flexbox gaps
- Updates margins to match design better
- Refactors tabs
- Refactor the event history page to match the new designs
- Fix layout with virtual lists
- Adds in event history views and their associated routing
- Adds pending activities to the event history list
- Adds support for formatting both history events and pending activities for the view

![image](https://user-images.githubusercontent.com/251000/147280778-b072065e-161a-4ea6-a72c-8ce7bca5de2d.png)

![image](https://user-images.githubusercontent.com/251000/147316116-11a31868-0c39-461e-ad7c-ac44911670f6.png)




Patch:
diff --git a/.gitignore b/.gitignore
index 1e79ef68..6deb09f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ node_modules
 /cypress/videos
 /build-local
 /build-cloud
+/coverage
diff --git a/.vscode/temporal-ui.code-snippets b/.vscode/temporal-ui.code-snippets
new file mode 100644
index 00000000..d50aacea
--- /dev/null
+++ b/.vscode/temporal-ui.code-snippets
@@ -0,0 +1,11 @@
+{
+	"Import Icons": {
+		"scope": "javascript,typescript",
+		"prefix": "fai",
+		"body": [
+			"import Icon from 'svelte-fa';",
+			"import { $0 } from '@fortawesome/free-solid-svg-icons';"
+		],
+		"description": "Imports Font Awesome icons"
+	}
+}
diff --git a/jest.config.js b/jest.config.js
index dea24647..ebc97ff0 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -13,6 +13,6 @@ export default {
     'src/types',
   ],
   reporters: ['default'],
-  globals: { 'ts-jest': { diagnostics: false } },
+  globals: { 'ts-jest': { diagnostics: false, isolatedModules: true } },
   transform: {},
 };
diff --git a/src/app.postcss b/src/app.postcss
index a16b7117..9bbc8480 100644
--- a/src/app.postcss
+++ b/src/app.postcss
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap');
 
 @tailwind base;
 
@@ -12,7 +12,7 @@ body {
 }
 
 body {
-  font-family: Inter, sans-serif;
+  font-family: Poppins, sans-serif;
   position: relative;
 }
 
diff --git a/src/events.d.ts b/src/events.d.ts
index 0c7212de..f9e2defc 100644
--- a/src/events.d.ts
+++ b/src/events.d.ts
@@ -303,17 +303,18 @@ type EventAttribute =
   | WorkflowTaskFailedAttrs
   | ChildWorkflowExecutionFailedAttrs;
 
-interface HistoryEventWithId
-  extends Omit<HistoryEvent, 'eventType'>,
-    Record<string, unknown> {
+interface HistoryEventWithId extends HistoryEvent {
   id: string;
   eventType: EventType;
   attributes: EventAttribute;
 }
 
-type PendingActivity = Omit<import('$types').PendingActivityInfo, 'state'> & {
+type PendingActivityInfo = import('$types').PendingActivityInfo;
+
+interface PendingActivity extends PendingActivityInfo {
   state: 'Unspecified' | 'Scheduled' | 'Started' | 'CancelRequested';
-} & Record<string, unknown>;
+  activityType: { name: string };
+}
 
 type EventType =
   | 'ActivityTaskCanceled'
@@ -357,6 +358,15 @@ type EventType =
   | 'MarkerRecorded'
   | 'UpsertWorkflowSearchAttributes';
 
+type ActivityType =
+  | 'ActivityTaskCanceled'
+  | 'ActivityTaskCancelRequested'
+  | 'ActivityTaskCompleted'
+  | 'ActivityTaskFailed'
+  | 'ActivityTaskScheduled'
+  | 'ActivityTaskStarted'
+  | 'ActivityTaskTimedOut';
+
 type EventTypeCategory =
   | 'activity'
   | 'child-workflow'
diff --git a/src/lib/components/code-block.svelte b/src/lib/components/code-block.svelte
index 04600a44..8041c6d9 100644
--- a/src/lib/components/code-block.svelte
+++ b/src/lib/components/code-block.svelte
@@ -1,14 +1,15 @@
 <script lang="ts">
   import { onMount } from 'svelte';
+  import Icon from 'svelte-fa';
+  import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
 
   export let heading = '';
   export let content: string | Parameters<typeof JSON.stringify>[0];
   export let copied = false;
-  export let inline = false;
 
   const copy = () =>
     navigator.clipboard
-      .writeText(content)
+      .writeText(formatJSON(JSON.stringify(content)))
       .then(() => {
         copied = !copied;
         setTimeout(() => (copied = false), 2000);
@@ -24,11 +25,7 @@
   onMount(() => window.Prism.highlightAll());
 </script>
 
-{#if inline}
-  <code class="language-json" style="white-space: nowrap;">
-    {formatJSON(JSON.stringify(content))}
-  </code>
-{:else if content || content === null}
+{#if content || content === null}
   <div class="relative w-full">
     <div id="clipboard" />
 
@@ -43,11 +40,11 @@
         </code>
       </pre>
 
-    <button on:click={copy}>
+    <button on:click={copy} class="absolute top-4 right-4 block">
       {#if copied}
-        <i class="fas fa-check" />
+        <Icon icon={faCheck} color="white" />
       {:else}
-        <i class="fas fa-clipboard" />
+        <Icon icon={faCopy} color="white" />
       {/if}
     </button>
   </div>
diff --git a/src/lib/components/data-convertor-status.svelte b/src/lib/components/data-convertor-status.svelte
index 64cccee6..6b7e44eb 100644
--- a/src/lib/components/data-convertor-status.svelte
+++ b/src/lib/components/data-convertor-status.svelte
@@ -1,13 +1,16 @@
 <script lang="ts">
   import Icon from 'svelte-fa';
+
   import {
     faLongArrowAltDown,
     faLongArrowAltUp,
   } from '@fortawesome/free-solid-svg-icons';
+
   import {
     dataConverterPort,
     lastDataConverterStatus,
   } from '$lib/stores/data-converter-config';
+
   import Tooltip from '$lib/components/tooltip.svelte';
 </script>
 
diff --git a/src/lib/components/event-details.svelte b/src/lib/components/event-details.svelte
index ece159ad..d64e37d6 100644
--- a/src/lib/components/event-details.svelte
+++ b/src/lib/components/event-details.svelte
@@ -1,29 +1,29 @@
 <script lang="ts">
   import { format } from '$lib/utilities/format-camel-case';
+
   import CodeBlock from './code-block.svelte';
 
-  export let attributes: EventAttribute | Record<string, unknown>;
+  export let attributes:
+    | EventAttribute
+    | PendingActivity
+    | Record<string, unknown>;
 </script>
 
-<section class="w-full px-8">
-  {#each Object.entries(attributes) as [key, value]}
-    <article
-      class="w-full flex items-center content-start py-4 border-t-2 border-gray-300"
-    >
-      <h4 class="w-96 flex-grow">{format(key)}</h4>
-      <div class="flex-grow w-full overflow-scroll">
-        {#if value === null}
-          <p class="font-mono whitespace-nowrap flex-grow">
-            {value}
-          </p>
-        {:else if typeof value === 'object'}
-          <div class="overflow-scroll flex-grow">
-            <CodeBlock content={value} inline={true} />
-          </div>
-        {:else if value}
-          <p class="whitespace-nowrap">{value}</p>
-        {/if}
-      </div>
-    </article>
-  {/each}
-</section>
+{#each Object.entries(attributes) as [key, value]}
+  <article
+    class="flex items-center content-start w-full py-4 border-t-2 first:border-t-0 border-gray-300"
+  >
+    <h4 class="w-96 flex-grow">{format(key)}</h4>
+    <div class="flex-grow w-full">
+      {#if value === null}
+        <p class="font-mono whitespace-nowrap flex-grow">
+          {value}
+        </p>
+      {:else if typeof value === 'object'}
+        <CodeBlock content={value} />
+      {:else if value}
+        <p><span class="bg-gray-300 text-gray-700 px-2">{value}</span></p>
+      {/if}
+    </div>
+  </article>
+{/each}
diff --git a/src/lib/components/event-label.svelte b/src/lib/components/event-label.svelte
index 639f4442..d358247b 100644
--- a/src/lib/components/event-label.svelte
+++ b/src/lib/components/event-label.svelte
@@ -4,13 +4,13 @@
   export let color: EventClassification | PendingActivity['state'] = null;
 </script>
 
-<span class="label {color}">
+<span class="label {color} font-semibold">
   <slot />
 </span>
 
 <style lang="postcss">
   .label {
-    @apply inline-block px-2 text-center text-gray-700 bg-gray-300;
+    @apply px-2 text-gray-700 bg-gray-300;
   }
 
   .Open,
diff --git a/src/lib/components/event-summary-attributes.svelte b/src/lib/components/event-summary-attributes.svelte
index 456938dc..73aa3802 100644
--- a/src/lib/components/event-summary-attributes.svelte
+++ b/src/lib/components/event-summary-attributes.svelte
@@ -6,12 +6,12 @@
   export let attributes: Record<string, unknown>;
 </script>
 
-<div class="flex items-center event gap-4 w-full h-full overflow-x-hidden">
+<div class="flex items-center event gap-4 w-full h-full">
   {#each Object.entries(attributes).filter(([, value]) => !!value) as [attribute, value]}
     <div class="flex gap-2">
       <h4 class="flex items-center">{format(attribute)}</h4>
       {#if typeof value === 'object'}
-        <CodeBlock content={value} inline={true} />
+        <CodeBlock content={value} />
       {:else}
         <p class="w-full label">{value}</p>
       {/if}
diff --git a/src/lib/components/event-summary.svelte b/src/lib/components/event-summary.svelte
index beb47044..0e356d5e 100644
--- a/src/lib/components/event-summary.svelte
+++ b/src/lib/components/event-summary.svelte
@@ -20,13 +20,9 @@
   };
 </script>
 
-<article
-  id={hash.slice(1)}
-  class="w-full py-2 my-4 border-2 rounded-lg relative"
-  on:click={expand}
->
+<article id={hash.slice(1)} class="w-full relative" on:click={expand}>
   <div class="flex justify-between m-4">
-    <div class="flex items-center gap-4 w-full h-full overflow-x-hidden">
+    <div class="flex items-center gap-4 w-full h-full">
       <slot />
     </div>
     <div class="flex justify-center items-center w-16">
diff --git a/src/lib/components/event-table.svelte b/src/lib/components/event-table.svelte
new file mode 100644
index 00000000..b10cc8be
--- /dev/null
+++ b/src/lib/components/event-table.svelte
@@ -0,0 +1,54 @@
+<script lang="ts">
+  import { setContext } from 'svelte';
+
+  import Event from './event.svelte';
+  import VirtualList from '@sveltejs/svelte-virtual-list';
+
+  export let events: (HistoryEventWithId | PendingActivity)[];
+</script>
+
+<section
+  class="flex flex-col border-2 border-gray-300 rounded-lg w-full event-history"
+>
+  <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="h-full rounded-bl-lg overflow-y-scroll">
+        <VirtualList items={events} let:item>
+          <Event event={item} />
+        </VirtualList>
+      </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>
+
+<style lang="postcss">
+  .event-history {
+    height: calc(100vh - 360px);
+  }
+
+  .table-header {
+    @apply bg-gray-100 text-gray-800 font-semibold p-4 border-b-2 flex justify-between items-center;
+  }
+</style>
diff --git a/src/lib/components/event.svelte b/src/lib/components/event.svelte
new file mode 100644
index 00000000..5fa8e3b8
--- /dev/null
+++ b/src/lib/components/event.svelte
@@ -0,0 +1,64 @@
+<script lang="ts">
+  import { page } from '$app/stores';
+
+  import Icon from 'svelte-fa';
+  import { faCalendar } from '@fortawesome/free-solid-svg-icons';
+
+  import { formatEvent } from '$lib/utilities/get-event-classification';
+
+  import EventLabel from '$lib/components/event-label.svelte';
+  import { formatDate } from '$lib/utilities/format-date';
+
+  export let event: HistoryEventWithId | PendingActivity;
+
+  let { id, pending, timeStamp, name, tag, classification } =
+    formatEvent(event);
+
+  let href = pending ? `pending-${id}` : `event-${id}`;
+</script>
+
+<a
+  {href}
+  sveltekit:noscroll
+  sveltekit:prefetch
+  class="flex border-b-2 border-gray-300 w-full items-center"
+  class:pending
+  class:active={$page.path.includes(href)}
+>
+  <article class="p-4 w-full">
+    <h2 class="mb-2 {tag}">
+      <EventLabel color={classification}>
+        {name}
+      </EventLabel>
+    </h2>
+    <p class="text-sm">
+      <Icon icon={faCalendar} class="inline" />
+      {formatDate(timeStamp)}
+    </p>
+  </article>
+  {#if pending}
+    <div class="mx-8 text-orange-600 italic">Pending</div>
+  {/if}
+</a>
+
+<style lang="postcss">
+  .active {
+    background: theme('colors.blue[100]');
+    /* Creates a border without modifying the shape and size of the element. */
+    background-image: linear-gradient(
+      90deg,
+      theme('colors.blue[700]') 0%,
+      theme('colors.blue[700]') 1%,
+      theme('colors.blue[100]') 1%,
+      theme('colors.blue[100]') 1%
+    );
+  }
+
+  .pending {
+    @apply bg-orange-50;
+  }
+
+  .pending .active {
+    @apply bg-blue-100;
+  }
+</style>
diff --git a/src/lib/components/select/filter-select.svelte b/src/lib/components/select/filter-select.svelte
index c8708814..a0c7c41a 100644
--- a/src/lib/components/select/filter-select.svelte
+++ b/src/lib/components/select/filter-select.svelte
@@ -4,7 +4,7 @@
   import { updateQueryParameters } from '$lib/utilities/update-query-parameters';
   import Select from './select.svelte';
 
-  export let label: string;
+  export let label: string = null;
   export let value: string;
   export let parameter: string = null;
 
@@ -19,7 +19,7 @@
       query: $page.query,
       path: $page.path,
       goto,
-    });
+    }).then((v) => (value = v));
   }
 </script>
 
diff --git a/src/lib/components/select/select.svelte b/src/lib/components/select/select.svelte
index a6a79797..a7c73dd0 100644
--- a/src/lib/components/select/select.svelte
+++ b/src/lib/components/select/select.svelte
@@ -10,7 +10,7 @@
   class="relative flex flex-col items-start justify-center border-gray-100 w-full"
 >
   <select
-    class="inline border-2 text-base p-2 w-full h-10 rounded-lg"
+    class="inline border-2 text-base px-2 w-full h-10 rounded-lg"
     class:dark
     class:remove={arrow}
     {id}
@@ -35,7 +35,7 @@
   }
 
   .arrow::after {
-    @apply absolute text-white text-sm font-normal top-2 right-3;
+    @apply text-white text-sm font-normal mr-4;
     font-family: 'Font Awesome 5 Free';
     content: '\f0dc';
   }
diff --git a/src/lib/components/tab.svelte b/src/lib/components/tab.svelte
new file mode 100644
index 00000000..15b4d764
--- /dev/null
+++ b/src/lib/components/tab.svelte
@@ -0,0 +1,22 @@
+<script lang="ts">
+  import { page } from '$app/stores';
+
+  export let href: string;
+  export let label: string;
+  export let amount: number | Long.Long = null;
+</script>
+
+<a class="block" class:active={$page.path.includes(href)} {href}>
+  {#if amount}
+    {label}
+    <span class="px-2 text-blue-700 bg-blue-100 rounded-sm">{amount}</span>
+  {:else}
+    {label}
+  {/if}
+</a>
+
+<style lang="postcss">
+  a.active {
+    @apply border-b-2 rounded-b-sm border-blue-700 font-medium;
+  }
+</style>
diff --git a/src/lib/components/toggle-button.svelte b/src/lib/components/toggle-button.svelte
index 7e2bec08..163019c3 100644
--- a/src/lib/components/toggle-button.svelte
+++ b/src/lib/components/toggle-button.svelte
@@ -1,23 +1,26 @@
 <script lang="ts">
   import Icon from 'svelte-fa';
   import { getContext } from 'svelte';
+  import { page } from '$app/stores';
 
   import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
+  import { appendQueryParameters } from '$lib/utilities/append-query-parameters';
 
   export let icon: IconDefinition;
   export let group = getContext<boolean>('group');
   export let scale = 1;
-  export let active = false;
+  export let href: string;
 </script>
 
-<button
-  class="py-2 px-4 border-2 block hover:text-white hover:bg-gray-600"
+<a
+  class="border-2 py-2 px-4 hover:text-white hover:bg-gray-600 flex items-center justify-center"
   class:rounded-lg={!group}
-  class:active
+  class:active={$page.path.includes(href)}
   class:group
+  href={appendQueryParameters(href, $page.query)}
 >
   <Icon {icon} {scale} />
-</button>
+</a>
 
 <style lang="postcss">
   .active {
diff --git a/src/lib/components/workflow-status.svelte b/src/lib/components/workflow-status.svelte
index 4d12b2ca..8e1d2489 100644
--- a/src/lib/components/workflow-status.svelte
+++ b/src/lib/components/workflow-status.svelte
@@ -26,7 +26,7 @@
   $: label = humanFriendlyNames[status];
 </script>
 
-<span class={`${color} rounded-xl text-center px-2 text-sm`}>
+<span class={`${color} rounded-md text-center px-2 text-sm`}>
   {label}
 </span>
 
diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts
index a3fcda81..44a1c569 100644
--- a/src/lib/services/workflow-service.ts
+++ b/src/lib/services/workflow-service.ts
@@ -33,12 +33,22 @@ const emptyWorkflowRequest = (): Promise<ListWorkflowExecutionsResponse> => {
   });
 };
 
+const checkForStatus =
+  (workflowStatus: WorkflowStatus, value: boolean) =>
+  ({ status }: FilterParameters): boolean => {
+    if (status === workflowStatus) return value;
+    return !value;
+  };
+
+const filterIsSetToRunning = checkForStatus('Running', true);
+const filterIsNotSetToRunning = checkForStatus('Running', false);
+
 export const fetchOpenWorkflows = (
   namespace: string,
   parameters: FilterParameters,
   request = fetch,
 ): Promise<ListWorkflowExecutionsResponse> => {
-  if (parameters.status !== 'Running') return emptyWorkflowRequest();
+  if (filterIsNotSetToRunning(parameters)) return emptyWorkflowRequest();
 
   const params = getWorkflowFilterParameters(parameters);
 
@@ -56,7 +66,7 @@ export const fetchClosedWorkflows = (
   parameters: FilterParameters,
   request = fetch,
 ): Promise<ListWorkflowExecutionsResponse> => {
-  if (parameters.status !== 'Running') return emptyWorkflowRequest();
+  if (filterIsSetToRunning(parameters)) return emptyWorkflowRequest();
 
   const params = getWorkflowFilterParameters(parameters);
 
diff --git a/src/lib/utilities/append-query-parameters.ts b/src/lib/utilities/append-query-parameters.ts
new file mode 100644
index 00000000..d0c9f24c
--- /dev/null
+++ b/src/lib/utilities/append-query-parameters.ts
@@ -0,0 +1,11 @@
+export const appendQueryParameters = (
+  href: string,
+  query: URLSearchParams,
+): string => {
+  if (!query) return href;
+  if (query.toString()) {
+    return `${href}?${query}`;
+  } else {
+    return href;
+  }
+};
diff --git a/src/lib/utilities/collect-activities.ts b/src/lib/utilities/collect-activities.ts
new file mode 100644
index 00000000..38d87a6b
--- /dev/null
+++ b/src/lib/utilities/collect-activities.ts
@@ -0,0 +1,105 @@
+type Activity = Record<ActivityType, HistoryEventWithId>;
+type Activities = Record<string, Activity>;
+type ActivityGroup = {
+  name?: string;
+  taskQueue?: string;
+  events: Activities;
+};
+
+const set = (
+  activities: ActivityGroup,
+  activityId: string | Long,
+  property: ActivityType,
+  value: HistoryEventWithId,
+): void => {
+  const id = String(activityId);
+  if (!activities[id]) {
+    const events = {} as Activities;
+    activities[id] = {
+      events,
+    } as ActivityGroup;
+  }
+  activities[id].events[property] = value;
+};
+
+const setMetadata = (
+  activities: ActivityGroup,
+  eventId: string | Long,
+  scheduledEvent: HistoryEventWithId,
+): void => {
+  const id = String(eventId);
+  const { activityType, taskQueue } =
+    scheduledEvent.activityTaskScheduledEventAttributes;
+
+  activities[id].name = activityType.name;
+  activities[id].taskQueue = taskQueue.name;
+};
+
+export const collectActivities = (events: HistoryEventWithId[]) => {
+  const activities: ActivityGroup = {
+    events: {} as Activities,
+  };
+
+  for (const event of events) {
+    if (event.eventType === 'ActivityTaskScheduled') {
+      set(activities, event.id, event.eventType, event);
+      setMetadata(activities, event.id, event);
+    }
+
+    if (event.eventType === 'ActivityTaskStarted') {
+      set(
+        activities,
+        event.activityTaskStartedEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+
+    if (event.eventType === 'ActivityTaskCanceled') {
+      set(
+        activities,
+        event.activityTaskCanceledEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+
+    if (event.eventType === 'ActivityTaskCancelRequested') {
+      set(
+        activities,
+        event.activityTaskCancelRequestedEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+
+    if (event.eventType === 'ActivityTaskFailed') {
+      set(
+        activities,
+        event.activityTaskFailedEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+
+    if (event.eventType === 'ActivityTaskTimedOut') {
+      set(
+        activities,
+        event.activityTaskTimedOutEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+
+    if (event.eventType === 'ActivityTaskCompleted') {
+      set(
+        activities,
+        event.activityTaskCompletedEventAttributes.scheduledEventId,
+        event.eventType,
+        event,
+      );
+    }
+  }
+
+  return activities;
+};
diff --git a/src/lib/utilities/format-date.ts b/src/lib/utilities/format-date.ts
index 1cb51156..a0a25723 100644
--- a/src/lib/utilities/format-date.ts
+++ b/src/lib/utilities/format-date.ts
@@ -9,7 +9,7 @@ export function formatDate(date: Date | Timestamp | string | null): string {
     date = timestampToDate(date);
   }
 
-  return format(parseJSON(date), 'MMMM dd, yyyy — h:mm a');
+  return format(parseJSON(date), 'h:mm:ss a O, E, MMM dd, yyyy');
 }
 
 function timestampToDate(ts: Timestamp): Date {
diff --git a/src/lib/utilities/get-event-classification.ts b/src/lib/utilities/get-event-classification.ts
index c78e4a8d..78ff0e7b 100644
--- a/src/lib/utilities/get-event-classification.ts
+++ b/src/lib/utilities/get-event-classification.ts
@@ -1,6 +1,18 @@
+import { format } from './format-camel-case';
+
 export type EventClassification = typeof eventClassifications[number];
 
+type EventSummary = {
+  id: string;
+  name: string;
+  timeStamp: string;
+  classification: EventClassification;
+  tag: string;
+  pending: boolean;
+};
+
 export const eventClassifications = [
+  'Unspecified',
   'Scheduled',
   'Open',
   'New',
@@ -17,13 +29,65 @@ export const eventClassifications = [
   '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 isActivity = (
+  event: HistoryEventWithId | PendingActivity,
+): 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 getEventClassification = (
-  event: HistoryEventWithId,
+  event: HistoryEventWithId | PendingActivity,
 ): EventClassification => {
-  const eventType = event.eventType.toString();
+  if (isActivity(event)) return event.state;
+
+  const eventType = event.eventType;
 
   if (eventType.includes('RequestCancel')) return 'CancelRequested';
   for (const classification of eventClassifications) {
     if (eventType.includes(classification)) return classification;
   }
 };
+
+const getName = (event: HistoryEventWithId | PendingActivity): string => {
+  if (isEvent(event)) return String(event.eventType);
+  if (isActivity(event)) return `${event.activityType.name}:${event.state}`;
+};
+
+const getTime = (event: HistoryEventWithId | PendingActivity): string => {
+  if (isEvent(event)) return String(event.eventTime);
+  if (isActivity(event)) return String(event.lastStartedTime);
+};
+
+const getId = (event: HistoryEventWithId | PendingActivity): string => {
+  if (isEvent(event)) return String(event.eventId);
+  if (isActivity(event)) return String(event.activityId);
+};
+
+export const formatEvent = (
+  event: HistoryEventWithId | PendingActivity,
+): EventSummary => {
+  return {
+    id: getId(event),
+    name: format(getName(event)),
+    timeStamp: getTime(event),
+    classification: getEventClassification(event),
+    tag: getName(event),
+    pending: isActivity(event),
+  };
+};
diff --git a/src/lib/utilities/update-query-parameters.ts b/src/lib/utilities/update-query-parameters.ts
index dbb68176..54ecaea8 100644
--- a/src/lib/utilities/update-query-parameters.ts
+++ b/src/lib/utilities/update-query-parameters.ts
@@ -1,4 +1,5 @@
 import type { goto, invalidate } from '$app/navigation';
+import { appendQueryParameters } from './append-query-parameters';
 
 type UpdateQueryParams = {
   parameter: string;
@@ -11,22 +12,20 @@ type UpdateQueryParams = {
 
 const options = { replaceState: true, keepfocus: true };
 
-export const updateQueryParameters = ({
+export const updateQueryParameters = async ({
   parameter,
   value,
   query,
   path,
   goto,
-}: UpdateQueryParams): void => {
+}: UpdateQueryParams): Promise<typeof value> => {
   if (value) {
     query.set(parameter, value);
   } else {
     query.delete(parameter);
   }
 
-  if (query.toString()) {
-    goto(`${path}?${query}`, options);
-  } else {
-    goto(path, options);
-  }
+  goto(appendQueryParameters(path, query), options);
+
+  return value;
 };
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index 46865105..e1f27146 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -44,10 +44,12 @@
   <meta property="og:image" content="/banner.png" />
 </svelte:head>
 
-<main class="h-screen overflow-y-hidden">
+<main class="flex flex-col h-screen gap-8">
   <Notifications />
   <Header />
-  <section id="content" class="h-full mx-10">
-    <slot />
+  <section id="content" class="h-full mx-10 mb-10">
+    <div class="flex flex-col h-full gap-4">
+      <slot />
+    </div>
   </section>
 </main>
diff --git a/src/routes/_header.svelte b/src/routes/_header.svelte
index fe5c4769..275aa559 100644
--- a/src/routes/_header.svelte
+++ b/src/routes/_header.svelte
@@ -20,25 +20,31 @@
   export let user: { name?: string; email?: string; picture?: string } = {};
 </script>
 
-<div
-  id="header"
-  class="h-16 w-full px-6 flex flex-row items-center justify-between bg-gray-900"
+<header
+  class="grid grid-rows-1 grid-cols-12 gap-8 px-10 items-center bg-gray-900 shadow-lg"
 >
-  <div class="flex flex-row justify-center items-center gap-4">
-    <img src="/logo.svg" alt="Temporal Logo" />
+  <div class="flex gap-4 col-span-3">
+    <a href="/" class="block">
+      <img src="/logo.svg" alt="Temporal Logo" class="max-h-10" />
+    </a>
     <NamespaceSelect />
-    <div class="flex flex-row">
-      <NavigationLink href={`/namespaces/${$namespace}/workflows`}>
-        Workflows
-      </NavigationLink>
-      <NavigationLink href={`/namespaces/${$namespace}/settings`}>
-        Settings
-      </NavigationLink>
-    </div>
   </div>
-  <div class="flex flex-row justify-center items-center">
+  <div class="flex gap-4 col-span-4">
+    <NavigationLink href={`/namespaces/${$namespace}/workflows`}>
+      Workflows
+    </NavigationLink>
+    <NavigationLink href={`/namespaces/${$namespace}/settings`}>
+      Settings
+    </NavigationLink>
+  </div>
+  <div class="col-span-2">
     <DataConvertorStatus />
-    <a href="https://github.com/temporalio/web/issues/new/choose">
+  </div>
+  <div class="flex justify-end gap-4 col-span-3">
+    <a
+      class="header-button"
+      href="https://github.com/temporalio/web/issues/new/choose"
+    >
       Report Bug/Give Feedback
     </a>
     {#if user.email}
@@ -46,24 +52,19 @@
         {user.email}
       </span>
     {:else}
-      <a href={import.meta.env.VITE_API + '/auth/sso'}> Sign In </a>
+      <a class="header-button" href={import.meta.env.VITE_API + '/auth/sso'}>
+        Sign In
+      </a>
     {/if}
   </div>
-</div>
+</header>
 
 <style lang="postcss">
-  #header {
-    @apply bg-gray-900 mb-0 shadow-lg;
-  }
-
-  img {
-    @apply w-8 mx-4;
-  }
-  a {
+  .header-button {
     @apply block px-4 py-2 text-sm text-white;
   }
 
-  a:hover {
+  .header-button:hover {
     @apply bg-purple-100 rounded-md text-gray-900;
   }
 </style>
diff --git a/src/routes/namespaces/[namespace]/queues/[queue]/__layout.svelte b/src/routes/namespaces/[namespace]/queue/[queue]/__layout.svelte
similarity index 96%
rename from src/routes/namespaces/[namespace]/queues/[queue]/__layout.svelte
rename to src/routes/namespaces/[namespace]/queue/[queue]/__layout.svelte
index 1a04e950..3bbf294a 100644
--- a/src/routes/namespaces/[namespace]/queues/[queue]/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/queue/[queue]/__layout.svelte
@@ -22,7 +22,7 @@
   <header class="p-3 mb-5">
     <h3 class="text-2xl">Pollers</h3>
   </header>
-  <div class="w-full h-full overflow-x-scroll">
+  <div class="w-full h-full">
     <QueuePollersTable>
       <tbody slot="rows">
         {#each pollers.pollers as poller}
diff --git a/src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-row.svelte b/src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-row.svelte
similarity index 100%
rename from src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-row.svelte
rename to src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-row.svelte
diff --git a/src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-table.svelte b/src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-table.svelte
similarity index 100%
rename from src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-table.svelte
rename to src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-table.svelte
diff --git a/src/routes/namespaces/[namespace]/queues/[queue]/index.svelte b/src/routes/namespaces/[namespace]/queue/[queue]/index.svelte
similarity index 100%
rename from src/routes/namespaces/[namespace]/queues/[queue]/index.svelte
rename to src/routes/namespaces/[namespace]/queue/[queue]/index.svelte
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
index 183a9e80..214a7ee6 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
@@ -25,7 +25,7 @@
   export let namespace: string;
 </script>
 
-<main class="w-full">
+<main class="flex flex-col gap-4 h-full">
   <Header {workflow} {namespace} />
   <slot />
 </main>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte
deleted file mode 100644
index 52309125..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte
+++ /dev/null
@@ -1,65 +0,0 @@
-<script lang="ts">
-  import { getAttributesFromEvent } from '$lib/utilities/get-attributes-from-event';
-  import { getEventClassification } from '$lib/utilities/get-event-classification';
-  import { format } from '$lib/utilities/format-camel-case';
-
-  import EventSummary from '$lib/components/event-summary.svelte';
-  import EventDetails from '$lib/components/event-details.svelte';
-  import EventSummaryAttributes from '$lib/components/event-summary-attributes.svelte';
-  import EventLabel from '$lib/components/event-label.svelte';
-
-  export let event: HistoryEventWithId;
-
-  const { attributes } = event;
-
-  const hash = `#event-${event.id}`;
-  const summaryEvent = getAttributesFromEvent(event);
-</script>
-
-<EventSummary {hash}>
-  <h2 class="w-1/3 {event.eventType}">
-    <EventLabel color={getEventClassification(event)}>
-      {format(String(event.eventType))}
-    </EventLabel>
-  </h2>
-  <EventSummaryAttributes attributes={summaryEvent.attributes} />
-  <EventDetails {attributes} slot="expanded" />
-</EventSummary>
-
-<style lang="postcss">
-  .label {
-    @apply bg-gray-300 px-2 rounded-sm;
-  }
-
-  .Scheduled {
-    @apply text-gray-700 bg-gray-300;
-  }
-
-  .Open,
-  .New {
-    @apply text-indigo-700 bg-indigo-100;
-  }
-
-  .Started,
-  .Initiated {
-    @apply text-blue-700 bg-blue-100;
-  }
-
-  .Running,
-  .Completed,
-  .Fired {
-    @apply text-green-700 bg-green-100;
-  }
-
-  .CancelRequested,
-  .TimedOut,
-  .Signaled,
-  .Cancelled {
-    @apply text-yellow-700 bg-yellow-100;
-  }
-
-  .Failed,
-  .Terminated {
-    @apply text-red-700 bg-red-100;
-  }
-</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
index 37aff0ee..08999b0d 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
@@ -1,4 +1,7 @@
 <script lang="ts">
+  import Icon from 'svelte-fa';
+  import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
+
   import type { WorkflowExecution } from '$lib/models/workflow-execution';
   import WorkflowStatus from '$lib/components/workflow-status.svelte';
   import TerminateWorkflow from '$lib/components/terminate-workflow.svelte';
@@ -8,32 +11,30 @@
   export let namespace: string;
 </script>
 
-<header class="flex flex-col justify-between">
-  <main class="px-6 mb-2">
-    <div class="flex m-0 mt-6 justify-between items-center">
-      <h1 class="text-lg">
-        <a href={`/namespaces/${namespace}/workflows`}>
-          <i
-            class="fas fa-arrow-left inline w-5 h-5 text-gray-500 cursor-pointer"
-          />
-        </a>
+<header class="flex flex-col gap-4">
+  <main class="flex flex-col gap-1 relative">
+    <a
+      href="/namespaces/{namespace}/workflows"
+      class="absolute top-2 back-to-workflows"
+      style="left: -1.5rem"
+    >
+      <Icon icon={faChevronLeft} />
+    </a>
+    <div class="flex justify-between items-center">
+      <h1 class="text-2xl">
         {workflow.name}
-        <span class="inline">
-          <WorkflowStatus status={workflow?.status} />
-        </span>
+        <WorkflowStatus status={workflow?.status} />
       </h1>
       <TerminateWorkflow {workflow} {namespace} />
     </div>
-    <p class="text-gray-500 text-xs">
-      <span class="uppercase text-gray-400 mr-2">Workflow ID</span>
-      {workflow.id}
+    <p class="text-md">
+      <span>Workflow ID</span>
+      <span class="font-medium">{workflow.id}</span>
+    </p>
+    <p class="text-md">
+      <span>Run ID</span>
+      <span class="font-medium">{workflow.runId}</span>
     </p>
   </main>
   <Tabs {workflow} />
 </header>
-
-<style lang="postcss">
-  p {
-    @apply ml-6;
-  }
-</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activities.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activities.svelte
deleted file mode 100644
index 202a5e03..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activities.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-<script lang="ts">
-  import Activity from './_pending-activity.svelte';
-
-  export let activities: PendingActivity[];
-  let showActivities = activities && activities.length;
-</script>
-
-{#if showActivities}
-  <div class="mb-6 w-full">
-    <h3 class="text-lg mb-2 font-semibold">Pending Activities</h3>
-    {#each activities as activity}
-      <Activity {activity} />
-    {/each}
-  </div>
-{/if}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activity.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activity.svelte
deleted file mode 100644
index 1902ce16..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_pending-activity.svelte
+++ /dev/null
@@ -1,33 +0,0 @@
-<script lang="ts">
-  import { pick } from '$lib/utilities/pick';
-
-  import EventSummary from '$lib/components/event-summary.svelte';
-  import EventDetails from '$lib/components/event-details.svelte';
-  import EventSummaryAttributes from '$lib/components/event-summary-attributes.svelte';
-  import EventLabel from '$lib/components/event-label.svelte';
-
-  export let activity: PendingActivity;
-
-  const summaryAttributes = pick<PendingActivity, keyof PendingActivity>(
-    activity,
-    'attempts',
-    'maximumAttempts',
-    'lastHeart',
-    'expiration',
-  );
-
-  const hash = `#pending-${activity.activityId}`;
-</script>
-
-<EventSummary {hash}>
-  <h2 class="w-1/3">
-    <EventLabel>
-      {activity?.activityType?.name}
-    </EventLabel>
-    <EventLabel color={activity.state}>
-      {activity.state}
-    </EventLabel>
-  </h2>
-  <EventSummaryAttributes attributes={summaryAttributes} />
-  <EventDetails attributes={activity} slot="expanded" />
-</EventSummary>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
index c835deed..15998bed 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
@@ -2,37 +2,23 @@
   import type { WorkflowExecution } from '$lib/models/workflow-execution';
 
   import { page } from '$app/stores';
+
   import { getWorkflowExecutionUrl } from '$lib/utilities/get-workflow-execution-url';
-  import { pathMatches } from '$lib/utilities/path-matches';
 
-  const { namespace, workflow: id, run: runId } = $page.params;
+  import Tab from '$lib/components/tab.svelte';
 
   export let workflow: WorkflowExecution;
 
-  $: workflowUrl = getWorkflowExecutionUrl(namespace, { id, runId });
+  const { namespace, workflow: id, run: runId } = $page.params;
   const historyEvents = workflow?.historyEvents;
+  const workflowUrl = getWorkflowExecutionUrl(namespace, { id, runId });
+
+  const urlFor = (path: string): string => `${workflowUrl}/${path}`;
 </script>
 
-<nav class="mt-6 px-6 flex">
-  <a
-    class:active={pathMatches(workflowUrl, $page.path, true)}
-    href={workflowUrl}
-  >
-    History <span class="inline px-2">{historyEvents}</span>
-  </a>
+<nav class="flex gap-6">
+  <Tab label="History" href={urlFor('history')} amount={historyEvents} />
+  <Tab label="Workers" href={urlFor('workers')} />
+  <Tab label="Stack Trace" href={urlFor('stack-trace')} />
+  <Tab label="Queries" href={urlFor('queries')} />
 </nav>
-
-<style lang="postcss">
-  a {
-    @apply block;
-  }
-
-  a.active {
-    @apply border-b-2 rounded-b-sm border-primary font-medium;
-  }
-
-  span {
-    background-color: #dce4fb;
-    color: #124ae5;
-  }
-</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
new file mode 100644
index 00000000..6b528390
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
@@ -0,0 +1,47 @@
+<script context="module" lang="ts">
+  import { getWorkflowExecutionUrl } from '$lib/utilities/get-workflow-execution-url';
+  import type { LoadInput } from '@sveltejs/kit';
+
+  export async function load({ stuff, page }: LoadInput) {
+    const { workflow, run: runId, namespace } = page.params;
+
+    const path =
+      getWorkflowExecutionUrl(namespace, { id: workflow, runId }) + '/history';
+
+    return {
+      props: {
+        path,
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import {
+    faCode,
+    faDownload,
+    faLayerGroup,
+    faStream,
+  } from '@fortawesome/free-solid-svg-icons';
+
+  import ToggleButton from '$lib/components/toggle-button.svelte';
+  import ToggleButtons from '$lib/components/toggle-buttons.svelte';
+
+  export let path: string;
+</script>
+
+<section class="flex flex-col gap-4">
+  <nav class="flex gap-4 justify-between items-end">
+    <h3 class="text-lg font-semibold">Event History</h3>
+    <div class="flex gap-4">
+      <ToggleButtons>
+        <ToggleButton icon={faStream} href="{path}/full" />
+        <ToggleButton icon={faLayerGroup} href="{path}/compact" />
+        <ToggleButton icon={faCode} href="{path}/json" />
+      </ToggleButtons>
+      <ToggleButton icon={faDownload} href="#no-implemented" />
+    </div>
+  </nav>
+</section>
+
+<slot />
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
new file mode 100644
index 00000000..114b1369
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
@@ -0,0 +1,46 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+
+  export async function load({ stuff }: LoadInput) {
+    const { events } = stuff as {
+      events: HistoryEventWithId[];
+    };
+
+    return {
+      props: {
+        events,
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import { eventTypeInCategory } from '$lib/utilities/get-event-categorization';
+
+  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 events: HistoryEventWithId[];
+
+  let category: EventTypeCategory = null;
+
+  $: visibleEvents = events.filter(eventTypeInCategory(category));
+</script>
+
+<EventTable events={visibleEvents}>
+  <div slot="filters">
+    <FilterSelect parameter="event-type" bind:value={category}>
+      <Option value={null}>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">
+    <slot />
+  </div>
+</EventTable>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[id].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[id].svelte
new file mode 100644
index 00000000..6dd56890
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[id].svelte
@@ -0,0 +1,3 @@
+<div class="flex items-center justify-center w-full h-full">
+  <p>(To be implemented.)</p>
+</div>
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
new file mode 100644
index 00000000..74b96bcc
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/index.svelte
@@ -0,0 +1,3 @@
+<div class="flex items-center justify-center w-full h-full">
+  <p>Select an activity 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
new file mode 100644
index 00000000..a19e3efe
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte
@@ -0,0 +1,51 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
+  export async function load({ stuff }: LoadInput) {
+    const { workflow, events } = stuff as {
+      workflow: WorkflowExecution;
+      events: HistoryEventWithId[];
+    };
+
+    return {
+      props: {
+        events,
+        pendingActivities: workflow.pendingActivities,
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import { eventTypeInCategory } from '$lib/utilities/get-event-categorization';
+
+  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 events: HistoryEventWithId[];
+  export let pendingActivities: PendingActivity[];
+
+  let category: EventTypeCategory = null;
+
+  $: visibleEvents = events.filter(eventTypeInCategory(category));
+  $: eventsAndActivities = [...pendingActivities, ...visibleEvents];
+</script>
+
+<EventTable events={eventsAndActivities}>
+  <div slot="filters">
+    <FilterSelect parameter="event-type" bind:value={category}>
+      <Option value={null}>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">
+    <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
new file mode 100644
index 00000000..5cc6c9fb
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/event-[id].svelte
@@ -0,0 +1,23 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+
+  export async function load({ stuff, page }: LoadInput) {
+    const { events } = stuff as {
+      events: HistoryEventWithId[];
+    };
+
+    return {
+      props: {
+        event: events.find((event) => event.id === page.params.id),
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import EventDetails from '$lib/components/event-details.svelte';
+
+  export let event: HistoryEventWithId;
+</script>
+
+<EventDetails attributes={event.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
new file mode 100644
index 00000000..ff0b5922
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/index.svelte
@@ -0,0 +1,3 @@
+<div class="flex items-center justify-center w-full h-full">
+  <p>Select an event 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
new file mode 100644
index 00000000..68b8a983
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/pending-[id].svelte
@@ -0,0 +1,28 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
+  export async function load({ stuff, page }: LoadInput) {
+    const { workflow } = stuff as {
+      workflow: WorkflowExecution;
+    };
+
+    const event = workflow.pendingActivities.find(
+      (activity) => activity.activityId === page.params.id,
+    );
+
+    return {
+      props: {
+        event,
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import EventDetails from '$lib/components/event-details.svelte';
+
+  export let event: PendingActivity;
+</script>
+
+<EventDetails attributes={event} />
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte
new file mode 100644
index 00000000..bbdac378
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/index.svelte
@@ -0,0 +1,5 @@
+<script context="module" lang="ts">
+  export async function load({ page }) {
+    return { status: 302, redirect: `${page.path}/full` };
+  }
+</script>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json/index.svelte
new file mode 100644
index 00000000..49d43ca2
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json/index.svelte
@@ -0,0 +1,22 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+
+  export async function load({ stuff, page }: LoadInput) {
+    const { events } = stuff as {
+      events: HistoryEventWithId[];
+    };
+
+    return {
+      props: {
+        events,
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import CodeBlock from '$lib/components/code-block.svelte';
+  export let events: HistoryEventWithId[];
+</script>
+
+<CodeBlock content={events} />
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
index b02c2757..7e4b29e3 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
@@ -1,79 +1,5 @@
 <script context="module" lang="ts">
-  import type { LoadInput } from '@sveltejs/kit';
-  import type { WorkflowExecution } from '$lib/models/workflow-execution';
-
-  export async function load({ stuff, page }: LoadInput) {
-    const { workflow, events } = stuff as {
-      workflow: WorkflowExecution;
-      events: HistoryEventWithId[];
-    };
-    const { namespace } = page.params;
-
-    return {
-      props: {
-        workflow,
-        events,
-        namespace,
-      },
-    };
+  export async function load({ page }) {
+    return { status: 302, redirect: `${page.path}/history` };
   }
 </script>
-
-<script lang="ts">
-  import {
-    faCode,
-    faDownload,
-    faLayerGroup,
-    faStream,
-  } from '@fortawesome/free-solid-svg-icons';
-
-  import { eventTypeInCategory } from '$lib/utilities/get-event-categorization';
-
-  import PendingActivities from './_pending-activities.svelte';
-  import Event from './_event.svelte';
-  import Option from '$lib/components/select/option.svelte';
-  import Select from '$lib/components/select/select.svelte';
-  import ToggleButton from '$lib/components/toggle-button.svelte';
-  import ToggleButtons from '$lib/components/toggle-buttons.svelte';
-
-  export let workflow: WorkflowExecution;
-  export let events: HistoryEventWithId[];
-
-  let pendingActivities = workflow?.pendingActivities;
-  let category: EventTypeCategory = null;
-
-  $: visibleEvents = events.filter(eventTypeInCategory(category));
-</script>
-
-<div class="execution-information px-6 py-6">
-  <div class="flex w-full mt-4">
-    <PendingActivities activities={pendingActivities} />
-  </div>
-  <section>
-    <h3 class="text-lg mb-2 font-semibold">Event History</h3>
-    <nav class="flex gap-4 justify-between">
-      <div class="w-36">
-        <Select bind:value={category}>
-          <Option value={null}>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>
-        </Select>
-      </div>
-      <div class="flex gap-4">
-        <ToggleButtons>
-          <ToggleButton icon={faStream} active />
-          <ToggleButton icon={faLayerGroup} />
-          <ToggleButton icon={faCode} />
-        </ToggleButtons>
-        <ToggleButton icon={faDownload} />
-      </div>
-    </nav>
-    {#each visibleEvents as event (event.id)}
-      <Event {event} />
-    {/each}
-  </section>
-</div>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/queries.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/queries.svelte
new file mode 100644
index 00000000..e69de29b
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/stack-trace.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/stack-trace.svelte
new file mode 100644
index 00000000..e69de29b
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte
new file mode 100644
index 00000000..e69de29b
diff --git a/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte b/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
index 0ad0d049..e959582f 100644
--- a/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
@@ -23,7 +23,7 @@
   const statuses = {
     All: null,
     Running: 'Running',
-    TimedOut: 'Timed Out',
+    'Timed Out': 'TimedOut',
     Completed: 'Completed',
     Failed: 'Failed',
     'Continued as New': 'ContinuedAsNew',
@@ -32,33 +32,30 @@
   };
 </script>
 
-<section class="flex flex-col my-8">
-  <h2 class="text-3xl mb-4">Workflows</h2>
-  <div class="grid grid-cols-5 gap-4">
-    <FilterInput
-      parameter="workflow-id"
-      name="Workflow ID"
-      value={workflowIdFilter}
-    />
-    <FilterInput
-      parameter="workflow-type"
-      name="Workflow Type"
-      value={workflowTypeFilter}
-    />
-    <FilterSelect label="Time Range" parameter="time-range" value="24 hours">
-      {#each durations as value}
-        <Option {value}>{value}</Option>
-      {/each}
-    </FilterSelect>
-    <FilterSelect label="Workflow Status" parameter="status" value={null}>
-      {#each Object.entries(statuses) as [label, value]}
-        <Option {value}>{label}</Option>
-      {/each}
-    </FilterSelect>
-    <Select id="filter-by-relative-time" bind:value={timeFormat}>
-      <Option value={'relative'}>Relative</Option>
-      <Option value={'UTC'}>UTC</Option>
-      <Option value={'current'}>Current</Option>
-    </Select>
-  </div>
-</section>
+<div class="grid grid-cols-5 gap-4">
+  <FilterInput
+    parameter="workflow-id"
+    name="Workflow ID"
+    value={workflowIdFilter}
+  />
+  <FilterInput
+    parameter="workflow-type"
+    name="Workflow Type"
+    value={workflowTypeFilter}
+  />
+  <FilterSelect label="Time Range" parameter="time-range" value="24 hours">
+    {#each durations as value}
+      <Option {value}>{value}</Option>
+    {/each}
+  </FilterSelect>
+  <FilterSelect label="Workflow Status" parameter="status" value={null}>
+    {#each Object.entries(statuses) as [label, value]}
+      <Option {value}>{label}</Option>
+    {/each}
+  </FilterSelect>
+  <Select id="filter-by-relative-time" bind:value={timeFormat}>
+    <Option value={'relative'}>Relative</Option>
+    <Option value={'UTC'}>UTC</Option>
+    <Option value={'current'}>Current</Option>
+  </Select>
+</div>
diff --git a/src/routes/namespaces/[namespace]/workflows/_workflows-summary-table.svelte b/src/routes/namespaces/[namespace]/workflows/_workflows-summary-table.svelte
index 4f002eaa..75a7eba2 100644
--- a/src/routes/namespaces/[namespace]/workflows/_workflows-summary-table.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/_workflows-summary-table.svelte
@@ -1,5 +1,5 @@
 <section
-  class="border-collapse border-2 border-gray-300 overflow-hidden w-full h-full rounded-lg"
+  class="flex flex-col border-2 border-gray-300 w-full rounded-lg workflow-table"
 >
   <div class="bg-gray-200 flex flex-row p-2">
     <div class="w-3/12 text-left">Workflow ID</div>
@@ -10,3 +10,9 @@
   </div>
   <slot />
 </section>
+
+<style lang="postcss">
+  .workflow-table {
+    height: calc(100vh - 280px);
+  }
+</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/index.svelte b/src/routes/namespaces/[namespace]/workflows/index.svelte
index 27b4ea9d..71ae6bf3 100644
--- a/src/routes/namespaces/[namespace]/workflows/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/index.svelte
@@ -42,21 +42,18 @@
   let timeFormat = 'relative';
 </script>
 
-<section class="h-full">
-  <header>
-    <WorkflowFilters bind:timeFormat />
-  </header>
-  {#await data}
-    <WorkflowsLoadingState />
-  {:then { workflows }}
-    {#if workflows.length}
-      <WorkflowsSummaryTable>
-        <VirtualList items={workflows} let:item>
-          <WorkflowsSummaryRow workflow={item} {timeFormat} />
-        </VirtualList>
-      </WorkflowsSummaryTable>
-    {:else}
-      <WorkflowsEmptyState />
-    {/if}
-  {/await}
-</section>
+<h2 class="text-2xl">Workflows</h2>
+<WorkflowFilters bind:timeFormat />
+{#await data}
+  <WorkflowsLoadingState />
+{:then { workflows }}
+  {#if workflows.length}
+    <WorkflowsSummaryTable>
+      <VirtualList items={workflows} let:item>
+        <WorkflowsSummaryRow workflow={item} {timeFormat} />
+      </VirtualList>
+    </WorkflowsSummaryTable>
+  {:else}
+    <WorkflowsEmptyState />
+  {/if}
+{/await}


Test patch:
diff --git a/src/lib/utilities/append-query-parameters.test.ts b/src/lib/utilities/append-query-parameters.test.ts
new file mode 100644
index 00000000..b4a69edb
--- /dev/null
+++ b/src/lib/utilities/append-query-parameters.test.ts
@@ -0,0 +1,21 @@
+import { appendQueryParameters } from './append-query-parameters';
+
+describe(appendQueryParameters, () => {
+  it('should append query paramters if available', () => {
+    const result = appendQueryParameters(
+      '/test',
+      new URLSearchParams({ parameter: 'value' }),
+    );
+    expect(result).toBe('/test?parameter=value');
+  });
+
+  it('should omit the question mark if there are no query parameters', () => {
+    const result = appendQueryParameters('/test', new URLSearchParams());
+    expect(result).toBe('/test');
+  });
+
+  it('should omit the question mark if null is purposely passed in', () => {
+    const result = appendQueryParameters('/test', null);
+    expect(result).toBe('/test');
+  });
+});
diff --git a/src/lib/utilities/collect-activities.test.ts b/src/lib/utilities/collect-activities.test.ts
new file mode 100644
index 00000000..26920e82
--- /dev/null
+++ b/src/lib/utilities/collect-activities.test.ts
@@ -0,0 +1,106 @@
+import { collectActivities } from './collect-activities';
+
+const eventHistory = [
+  {
+    id: '5',
+    eventId: '5',
+    eventTime: '2021-12-27T15:23:21.806965200Z',
+    eventType: 'ActivityTaskScheduled',
+    version: '0',
+    taskId: '1049493',
+    activityTaskScheduledEventAttributes: {
+      activityId: '5',
+      activityType: {
+        name: 'CompletedActivity',
+      },
+      namespace: '',
+      taskQueue: {
+        name: 'rainbow-statuses',
+        kind: 'Normal',
+      },
+      header: {
+        fields: {},
+      },
+      input: null,
+      scheduleToCloseTimeout: '0s',
+      scheduleToStartTimeout: '0s',
+      startToCloseTimeout: '3600s',
+      heartbeatTimeout: '0s',
+      workflowTaskCompletedEventId: '4',
+      retryPolicy: {
+        initialInterval: '1s',
+        backoffCoefficient: 2,
+        maximumInterval: '100s',
+        maximumAttempts: 1,
+        nonRetryableErrorTypes: [],
+      },
+    },
+  },
+  {
+    id: '6',
+    eventId: '6',
+    eventTime: '2021-12-27T15:23:21.824600700Z',
+    eventType: 'ActivityTaskStarted',
+    version: '0',
+    taskId: '1049497',
+    activityTaskStartedEventAttributes: {
+      scheduledEventId: '5',
+      identity: '21665@temporal@',
+      requestId: '0202572f-485c-426a-a3ee-02ea46d3cad7',
+      attempt: 1,
+      lastFailure: null,
+    },
+  },
+  {
+    id: '7',
+    eventId: '7',
+    eventTime: '2021-12-27T15:23:21.841178500Z',
+    eventType: 'ActivityTaskCompleted',
+    version: '0',
+    taskId: '1049498',
+    activityTaskCompletedEventAttributes: {
+      result: null,
+      scheduledEventId: '5',
+      startedEventId: '6',
+      identity: '21665@temporal@',
+    },
+  },
+];
+
+describe(collectActivities, () => {
+  it('have the activity name', () => {
+    const result = collectActivities(eventHistory);
+    const activity = result[5];
+
+    expect(activity.name).toBe('CompletedActivity');
+  });
+
+  it('should have a key with the ID of the ActivityTaskScheduledEvent', () => {
+    const result = collectActivities(eventHistory);
+    expect(result[5]).toBeDefined();
+  });
+
+  it('should have a scheduled activity of the first event', () => {
+    const [activityScheduled] = eventHistory;
+    const result = collectActivities(eventHistory);
+    const activity = result[5];
+
+    expect(activity.events.ActivityTaskScheduled).toBe(activityScheduled);
+  });
+
+  it('should have a scheduled activity of the first event', () => {
+    const [, activityStarted] = eventHistory;
+    const result = collectActivities(eventHistory);
+    const activity = result[5];
+
+    expect(activity.events.ActivityTaskStarted).toBe(activityStarted);
+  });
+
+  it('should have a scheduled activity of the first event', () => {
+    const [, , activityCompleted] = eventHistory;
+    const result = collectActivities(eventHistory);
+    const activity = result[5];
+
+    expect(activity.events.ActivityTaskCompleted).toBe(activityCompleted);
+  });
+});
diff --git a/src/lib/utilities/get-event-classification.test.ts b/src/lib/utilities/get-event-classification.test.ts
index 3d8921ab..c0cee89f 100644
--- a/src/lib/utilities/get-event-classification.test.ts
+++ b/src/lib/utilities/get-event-classification.test.ts
@@ -1,4 +1,9 @@
-import { getEventClassification } from './get-event-classification';
+import {
+  getEventClassification,
+  isEvent,
+  isActivity,
+  formatEvent,
+} from './get-event-classification';
 
 describe(getEventClassification, () => {
   it('should return "Started" for WorkflowExecutionStartedEvent', () => {
@@ -275,3 +280,82 @@ describe(getEventClassification, () => {
     ).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(isActivity, () => {
+  it('should return true if the event has an eventType', () => {
+    expect(isActivity({ activityType: { name: 'Workflow Started' } })).toBe(
+      true,
+    );
+  });
+
+  it('should return false if the event does not have an eventType', () => {
+    expect(isActivity({ eventType: 'Workflow Started' })).toBe(false);
+  });
+
+  it('should return false if passed null', () => {
+    expect(isActivity(null)).toBe(false);
+  });
+
+  it('should return false if passed a string', () => {
+    expect(isActivity('string')).toBe(false);
+  });
+
+  it('should return false if passed a number', () => {
+    expect(isActivity(4)).toBe(false);
+  });
+
+  it('should return false if passed an array', () => {
+    expect(isActivity([])).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');
+  });
+});