Repository: temporalio/ui
Pull request #171 (2021-12-14 22:49:10)

Add pipeline to update ui-server on changes
<!--- Note to EXTERNAL Contributors -->
<!-- Thanks for opening a PR! 
If it is a significant code change, please **make sure there is an open issue** for this. 
We work best with you when we have accepted the idea first before you code. -->

<!--- For ALL Contributors 👇 -->

## What was changed
<!-- Describe what has changed in this PR -->

Adds pipeline that notifies ui-server about new commits

## Why?
<!-- Tell your future self why have you made these changes -->

Automating UI updates

## Checklist
<!--- add/delete as needed --->

1. Closes <!-- add issue number here -->

2. How was this tested:
<!--- Please describe how you tested your changes/how we can test them -->

Tested with test repositories https://github.com/feedmeapples/test2
tbu. testing in current repo by manually running pipelines

3. Any docs updates needed?
<!--- update README if applicable
      or point out where to update docs.temporal.io -->



Patch:
diff --git a/.github/workflows/trigger-ui-server-update.yml b/.github/workflows/trigger-ui-server-update.yml
new file mode 100644
index 00000000..72a9b279
--- /dev/null
+++ b/.github/workflows/trigger-ui-server-update.yml
@@ -0,0 +1,26 @@
+name: 'Trigger ui-server Update'
+
+on:
+  push:
+    branches:
+      - main
+  workflow_dispatch:
+
+jobs:
+  notify:
+    name: 'trigger ui-server update'
+    runs-on: ubuntu-latest
+
+    defaults:
+      run:
+        shell: bash
+
+    steps:
+      - name: Dispatch ui-server Github Action
+        env:
+          PAT: ${{ secrets.PAT }}
+          PARENT_REPO: temporalio/ui-server
+          PARENT_BRANCH: main
+          WORKFLOW_ID: update-ui.yml
+        run: |
+          curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ env.PAT }}" https://api.github.com/repos/${{ env.PARENT_REPO }}/actions/workflows/${{ env.WORKFLOW_ID }}/dispatches -d '{"ref":"${{ env.PARENT_BRANCH }}"}'
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/settings.json b/.vscode/settings.json
index dde673e0..6d55db32 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,6 @@
     "**/.svelte-kit": true,
     "**/build-local": true,
     "**/build-cloud": true
-  }
+  },
+  "jest.nodeEnv": { "TZ": "UTC" }
 }
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 c2a0a116..ebc97ff0 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,5 @@
+process.env.TZ = 'UTC';
+
 export default {
   preset: 'ts-jest',
   testEnvironment: 'node',
@@ -11,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/package-lock.json b/package-lock.json
index e7e847b4..c74d476f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,12 +8,12 @@
       "name": "temporal-ui",
       "version": "0.0.1",
       "dependencies": {
-        "@sveltejs/svelte-virtual-list": "^3.0.1",
         "@fortawesome/free-solid-svg-icons": "^5.15.4",
+        "@sveltejs/svelte-virtual-list": "^3.0.1",
         "date-fns": "^2.23.0",
         "json-beautify": "^1.1.1",
+        "just-debounce": "^1.1.0",
         "svelte-fa": "^2.4.0",
-        "svelte-hero-icons": "^3.0.0",
         "url-pattern": "^1.0.3",
         "websocket-as-promised": "^2.0.1"
       },
@@ -6274,6 +6274,11 @@
         "node": ">=0.6.0"
       }
     },
+    "node_modules/just-debounce": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz",
+      "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ=="
+    },
     "node_modules/kleur": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
@@ -8579,14 +8584,6 @@
       "resolved": "https://registry.npmjs.org/svelte-fa/-/svelte-fa-2.4.0.tgz",
       "integrity": "sha512-0bnbMGbsE1LUnlioDcf27tl2O8kjuXlTXMXzIxC7LoIOWmqn0D+zd539HfLiQbdLuOHGTaynwN9V+4ehhEu1Jw=="
     },
-    "node_modules/svelte-hero-icons": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/svelte-hero-icons/-/svelte-hero-icons-3.0.0.tgz",
-      "integrity": "sha512-QivdVBK5dhUaxGq+GwBCW2EkhCFun4I1kJ22lu9b3dz7M2YJQXWsjwsNu4FIVQ1wBuPkaHAg9mdZeM7KV8fDIA==",
-      "dependencies": {
-        "svelte-hero-icons": "^3.0.0-beta.3"
-      }
-    },
     "node_modules/svelte-highlight": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-3.4.0.tgz",
@@ -14387,6 +14384,11 @@
         "verror": "1.10.0"
       }
     },
+    "just-debounce": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz",
+      "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ=="
+    },
     "kleur": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
@@ -16041,14 +16043,6 @@
       "resolved": "https://registry.npmjs.org/svelte-fa/-/svelte-fa-2.4.0.tgz",
       "integrity": "sha512-0bnbMGbsE1LUnlioDcf27tl2O8kjuXlTXMXzIxC7LoIOWmqn0D+zd539HfLiQbdLuOHGTaynwN9V+4ehhEu1Jw=="
     },
-    "svelte-hero-icons": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/svelte-hero-icons/-/svelte-hero-icons-3.0.0.tgz",
-      "integrity": "sha512-QivdVBK5dhUaxGq+GwBCW2EkhCFun4I1kJ22lu9b3dz7M2YJQXWsjwsNu4FIVQ1wBuPkaHAg9mdZeM7KV8fDIA==",
-      "requires": {
-        "svelte-hero-icons": "^3.0.0-beta.3"
-      }
-    },
     "svelte-highlight": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-3.4.0.tgz",
diff --git a/package.json b/package.json
index d8dbc15f..4c592006 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
     "dev:cloud": "TEMPORAL_UI_BUILD_TARGET=cloud svelte-kit dev",
     "build:local": "TEMPORAL_UI_BUILD_TARGET=local svelte-kit build",
     "build:cloud": "TEMPORAL_UI_BUILD_TARGET=cloud svelte-kit build",
-    "test": "jest",
+    "test": "TZ=UTC jest",
     "preview:local": "TEMPORAL_UI_BUILD_TARGET=local svelte-kit preview",
     "preview:cloud": "TEMPORAL_UI_BUILD_TARGET=cloud svelte-kit preview",
     "check": "TEMPORAL_UI_BUILD_TARGET=local svelte-check --tsconfig ./tsconfig.json",
@@ -22,12 +22,12 @@
     "cypress:open": "TEMPORAL_API_HOST=http://localhost:3000 cypress open"
   },
   "dependencies": {
-    "@sveltejs/svelte-virtual-list": "^3.0.1",
     "@fortawesome/free-solid-svg-icons": "^5.15.4",
+    "@sveltejs/svelte-virtual-list": "^3.0.1",
     "date-fns": "^2.23.0",
     "json-beautify": "^1.1.1",
+    "just-debounce": "^1.1.0",
     "svelte-fa": "^2.4.0",
-    "svelte-hero-icons": "^3.0.0",
     "url-pattern": "^1.0.3",
     "websocket-as-promised": "^2.0.1"
   },
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..4fbb984a 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'
@@ -364,3 +374,7 @@ type EventTypeCategory =
   | 'timer'
   | 'workflow'
   | 'command';
+
+type ActivityEvent = HistoryEventWithId & {
+  eventType: ActivityType;
+};
diff --git a/src/global.d.ts b/src/global.d.ts
index 85f6ac41..db507347 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -74,3 +74,16 @@ type Activity = {
   activityTaskCancelRequestedEvent?: ActivityTaskCancelRequestedEvent;
   activityTaskCanceledEvent?: ActivityTaskCanceledEvent;
 };
+
+type FilterParameters = {
+  workflowId?: string;
+  workflowType?: string;
+  status?: WorkflowStatus;
+  timeRange?: Duration | string;
+};
+
+type Settings = {
+  auth: {
+    enabled: boolean;
+  };
+};
diff --git a/src/lib/components/banner.svelte b/src/lib/components/banner.svelte
new file mode 100644
index 00000000..c279e4e8
--- /dev/null
+++ b/src/lib/components/banner.svelte
@@ -0,0 +1,52 @@
+<script lang="ts">
+  import { closedBannerId, close } from '$lib/stores/banner';
+  import { cluster } from '$lib/stores/cluster';
+
+  const severities = {
+    High: 'high',
+    Medium: 'medium',
+    Low: 'low',
+  };
+
+  $: recommended = $cluster?.versionInfo?.recommended;
+  $: current = $cluster?.versionInfo?.current;
+  $: alert = $cluster?.versionInfo?.alerts?.[0];
+  $: severity = alert ? severities[alert.severity] : severities.Low;
+  $: show = recommended?.version && recommended.version != $closedBannerId;
+  $: message =
+    severity == severities.Low
+      ? `📥 v${recommended?.version} version is available`
+      : `📥 ${alert?.message}`;
+</script>
+
+{#if show}
+  <section class={`block leading-10 text-center ${severity}`}>
+    <a
+      href="https://github.com/temporalio/temporal/releases/tag/v{$cluster
+        .versionInfo.recommended.version}"
+      target="_blank"
+    >
+      {message}
+    </a>
+    <button
+      on:click={() => close(current.version)}
+      class="absolute top-0 right-0 mr-5 text-black-600"
+    >
+      ✕
+    </button>
+  </section>
+{/if}
+
+<style lang="postcss">
+  .high {
+    @apply bg-red-200 text-red-900;
+  }
+
+  .medium {
+    @apply bg-orange-200 text-orange-900;
+  }
+
+  .low {
+    @apply bg-blue-100 text-blue-900;
+  }
+</style>
diff --git a/src/lib/components/code-block.svelte b/src/lib/components/code-block.svelte
index 850f1af2..8041c6d9 100644
--- a/src/lib/components/code-block.svelte
+++ b/src/lib/components/code-block.svelte
@@ -1,16 +1,15 @@
 <script lang="ts">
   import { onMount } from 'svelte';
-  import Icon from 'svelte-hero-icons/Icon.svelte';
-  import { Clipboard, Check } from 'svelte-hero-icons';
+  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);
@@ -26,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" />
 
@@ -45,17 +40,11 @@
         </code>
       </pre>
 
-    <button on:click={copy}>
+    <button on:click={copy} class="absolute top-4 right-4 block">
       {#if copied}
-        <Icon
-          src={Check}
-          class="w-8 h-8 text-purple-900 bg-gray-300 border-2 border-gray-200 absolute right-0 top-0 hidden group-hover:block hover:bg-gray-400 hover:border-gray-400"
-        />
+        <Icon icon={faCheck} color="white" />
       {:else}
-        <Icon
-          src={Clipboard}
-          class="w-8 h-8 text-purple-900 bg-gray-300 border-2 border-gray-200 absolute right-0 top-0 hidden group-hover:block hover:bg-gray-400 hover:border-gray-400"
-        />
+        <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 8ad596b6..6b7e44eb 100644
--- a/src/lib/components/data-convertor-status.svelte
+++ b/src/lib/components/data-convertor-status.svelte
@@ -1,25 +1,64 @@
 <script lang="ts">
-  import { StatusOnline, StatusOffline } from 'svelte-hero-icons';
+  import Icon from 'svelte-fa';
+
+  import {
+    faLongArrowAltDown,
+    faLongArrowAltUp,
+  } from '@fortawesome/free-solid-svg-icons';
+
   import {
     dataConverterPort,
     lastDataConverterStatus,
   } from '$lib/stores/data-converter-config';
-  import Icon from 'svelte-hero-icons/Icon.svelte';
+
   import Tooltip from '$lib/components/tooltip.svelte';
 </script>
 
 {#if $dataConverterPort}
   {#if $lastDataConverterStatus === 'notRequested'}
     <Tooltip text={'Data converter is configured'}>
-      <Icon src={StatusOffline} class="mx-auto text-gray-400 w-8 h-8" />
+      <div class="flex">
+        <Icon
+          icon={faLongArrowAltDown}
+          scale={1}
+          class="text-blue-200 block w-full h-full"
+        />
+        <Icon
+          icon={faLongArrowAltUp}
+          scale={1}
+          class="text-blue-200 block w-full h-full"
+        />
+      </div>
     </Tooltip>
   {:else if $lastDataConverterStatus === 'error'}
     <Tooltip text={`Data converter couldn't connect to the remote converter`}>
-      <Icon src={StatusOffline} class="mx-auto text-red-400 w-8 h-8" />
+      <div class="flex">
+        <Icon
+          icon={faLongArrowAltDown}
+          scale={1}
+          class="text-red-400 block w-full h-full"
+        />
+        <Icon
+          icon={faLongArrowAltUp}
+          scale={1}
+          class="text-red-400 block w-full h-full"
+        />
+      </div>
     </Tooltip>
   {:else if $lastDataConverterStatus === 'success'}
     <Tooltip text={'Data converter succesfully converted content'}>
-      <Icon src={StatusOnline} class="mx-auto text-green-500 w-8 h-8" />
+      <div class="flex">
+        <Icon
+          icon={faLongArrowAltDown}
+          scale={1}
+          class="text-blue-200 block w-full h-full"
+        />
+        <Icon
+          icon={faLongArrowAltUp}
+          scale={1}
+          class="text-blue-200 block w-full h-full"
+        />
+      </div>
     </Tooltip>
   {/if}
 {/if}
diff --git a/src/lib/components/empty-state.svelte b/src/lib/components/empty-state.svelte
new file mode 100644
index 00000000..fc533e1a
--- /dev/null
+++ b/src/lib/components/empty-state.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+  import Icon from 'svelte-fa';
+  import { faMeteor } from '@fortawesome/free-solid-svg-icons';
+
+  export let title: string;
+  export let content: string = '';
+</script>
+
+<div class="mt-12 flex flex-col justify-start items-center gap-2">
+  <span
+    class="flex rounded-full items-center justify-center w-16 h-16 bg-gray-200"
+  >
+    <Icon icon={faMeteor} scale={1.2} class="block w-full h-full" /></span
+  >
+  <h2 class="text-xl font-medium">{title}</h2>
+  {#if content}
+    <p class="text-center">{content}</p>
+  {/if}
+</div>
diff --git a/src/lib/components/event-details.svelte b/src/lib/components/event-details.svelte
index ece159ad..ea556286 100644
--- a/src/lib/components/event-details.svelte
+++ b/src/lib/components/event-details.svelte
@@ -1,29 +1,31 @@
 <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 border-b-2 last:border-t-0 border-gray-300 py-4"
+  >
+    <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>
+      {:else}
+        <p class="text-gray-500">Undefined</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..46820d62
--- /dev/null
+++ b/src/lib/components/event-table.svelte
@@ -0,0 +1,54 @@
+<script lang="ts">
+  import VirtualList from '@sveltejs/svelte-virtual-list';
+
+  import type { Activities } from '$lib/models/activity';
+  import Event from './event.svelte';
+
+  export let events: (HistoryEventWithId | PendingActivity)[] | Activities;
+</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..b33029b0
--- /dev/null
+++ b/src/lib/components/event.svelte
@@ -0,0 +1,67 @@
+<script lang="ts">
+  import type { Activity } from '$lib/models/activity';
+  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 { formatDate } from '$lib/utilities/format-date';
+
+  import EventLabel from '$lib/components/event-label.svelte';
+
+  export let event: HistoryEventWithId | PendingActivity | Activity;
+
+  let { id, routeFor, pending, timeStamp, name, tag, classification } =
+    formatEvent(event);
+
+  let { namespace, workflow: workflowId, run: runId } = $page.params;
+  let parameters = { namespace, workflowId, runId, eventId: id };
+  let href = routeFor(parameters);
+</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/filter-input.svelte b/src/lib/components/filter-input.svelte
index 397a81c1..f602eb97 100644
--- a/src/lib/components/filter-input.svelte
+++ b/src/lib/components/filter-input.svelte
@@ -1,7 +1,27 @@
 <script lang="ts">
-  export let id: string;
+  import { goto } from '$app/navigation';
+  import { page } from '$app/stores';
+  import { updateQueryParameters } from '$lib/utilities/update-query-parameters';
+  import debounce from 'just-debounce';
+
+  export let parameter: string;
   export let name: string;
   export let value: string;
+
+  let _value = (parameter && $page.query.get(parameter)) || value;
+
+  const id = `${parameter || name}-filter`;
+  const update = debounce(updateQueryParameters, 300);
+
+  $: {
+    update({
+      parameter,
+      value: _value,
+      query: $page.query,
+      path: $page.path,
+      goto,
+    });
+  }
 </script>
 
 <div class="flex flex-col items-start justify-center">
@@ -10,6 +30,6 @@
     class="block border-2 text-base p-2 w-full h-10 rounded-lg "
     placeholder={name}
     {id}
-    bind:value
+    bind:value={_value}
   />
 </div>
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/namespace-select.svelte b/src/lib/components/select/namespace-select.svelte
index 7c261de4..08985ebe 100644
--- a/src/lib/components/select/namespace-select.svelte
+++ b/src/lib/components/select/namespace-select.svelte
@@ -18,7 +18,7 @@
   }
 </script>
 
-<Select bind:value={selectedNamespace} on:change={() => switchNamespace()}>
+<Select dark bind:value={selectedNamespace} on:change={switchNamespace}>
   {#each namespaces as namespace}
     <Option value={namespace}>{namespace}</Option>
   {/each}
diff --git a/src/lib/components/select/select.svelte b/src/lib/components/select/select.svelte
index 51a3ce61..a7c73dd0 100644
--- a/src/lib/components/select/select.svelte
+++ b/src/lib/components/select/select.svelte
@@ -1,11 +1,18 @@
 <script lang="ts">
   export let id: string = null;
   export let value: string | boolean;
+  export let dark: boolean = false;
+  export let arrow: boolean = false;
 </script>
 
-<div class="flex flex-col items-start justify-center border-gray-100 w-full">
+<div
+  class:arrow
+  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}
     bind:value
     on:change
@@ -16,6 +23,20 @@
 
 <style lang="postcss">
   select {
-    color: gray;
+    @apply text-gray-500;
+  }
+
+  .remove {
+    @apply appearance-none h-8 text-sm py-1 pl-3 rounded-xl;
+  }
+
+  .dark {
+    @apply bg-gray-900 text-white;
+  }
+
+  .arrow::after {
+    @apply text-white text-sm font-normal mr-4;
+    font-family: 'Font Awesome 5 Free';
+    content: '\f0dc';
   }
 </style>
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
new file mode 100644
index 00000000..163019c3
--- /dev/null
+++ b/src/lib/components/toggle-button.svelte
@@ -0,0 +1,41 @@
+<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 href: string;
+</script>
+
+<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={$page.path.includes(href)}
+  class:group
+  href={appendQueryParameters(href, $page.query)}
+>
+  <Icon {icon} {scale} />
+</a>
+
+<style lang="postcss">
+  .active {
+    @apply bg-gray-900 text-white hover:bg-gray-900;
+  }
+
+  .group:first-child {
+    @apply rounded-tl-lg rounded-bl-lg;
+  }
+
+  .group:not(:last-child) {
+    @apply border-r-0;
+  }
+
+  .group:last-child {
+    @apply rounded-tr-lg rounded-br-lg;
+  }
+</style>
diff --git a/src/lib/components/toggle-buttons.svelte b/src/lib/components/toggle-buttons.svelte
new file mode 100644
index 00000000..08e1d9ce
--- /dev/null
+++ b/src/lib/components/toggle-buttons.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+  import { setContext } from 'svelte';
+  setContext<boolean>('group', true);
+</script>
+
+<div class="flex">
+  <slot />
+</div>
diff --git a/src/lib/components/workers-row.svelte b/src/lib/components/workers-row.svelte
new file mode 100644
index 00000000..210f63d4
--- /dev/null
+++ b/src/lib/components/workers-row.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+  import Icon from 'svelte-fa';
+  import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
+  import Time from '$lib/components/workflow-time.svelte';
+
+  export let poller;
+</script>
+
+<article class="w-full h-full flex flex-row border-b-2 no-underline p-2">
+  <div class="links w-3/12 text-left">
+    {poller.identity}
+  </div>
+  <div class="links w-3/12 text-left">
+    <h3>
+      <Time time={poller.lastAccessTime} timeFormat={'current'} />
+    </h3>
+  </div>
+  <div class="w-3/12 text-left">
+    {#if poller.taskQueueTypes.includes('WORKFLOW')}
+      <Icon icon={faCheck} color="blue" />
+    {:else}
+      <Icon icon={faTimes} color="black" />
+    {/if}
+  </div>
+  <div class="w-3/12 text-left">
+    {#if poller.taskQueueTypes.includes('ACTIVITY')}
+      <Icon icon={faCheck} color="blue" />
+    {:else}
+      <Icon icon={faTimes} color="black" />
+    {/if}
+  </div>
+</article>
diff --git a/src/lib/components/workers-table.svelte b/src/lib/components/workers-table.svelte
new file mode 100644
index 00000000..54f135db
--- /dev/null
+++ b/src/lib/components/workers-table.svelte
@@ -0,0 +1,9 @@
+<section class="flex flex-col border-2 border-gray-300 w-full rounded-lg">
+  <div class="bg-gray-200 flex flex-row p-2">
+    <div class="w-3/12 text-left">ID</div>
+    <div class="w-3/12 text-left">Last Accessed</div>
+    <div class="w-3/12 text-left">Workflow Table Handler</div>
+    <div class="w-2/12 text-left">Activity Handler</div>
+  </div>
+  <slot />
+</section>
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/models/activity.ts b/src/lib/models/activity.ts
new file mode 100644
index 00000000..90832557
--- /dev/null
+++ b/src/lib/models/activity.ts
@@ -0,0 +1,147 @@
+type ScheduledActivityEvent = HistoryEventWithId & {
+  eventType: 'ActivityTaskScheduled';
+};
+
+const activityTypes = [
+  'ActivityTaskCanceled',
+  'ActivityTaskCancelRequested',
+  'ActivityTaskCompleted',
+  'ActivityTaskFailed',
+  'ActivityTaskScheduled',
+  'ActivityTaskStarted',
+  'ActivityTaskTimedOut',
+];
+
+const isActivity = (event: HistoryEventWithId): event is ActivityEvent => {
+  if (activityTypes.includes(event.eventType)) return true;
+  return false;
+};
+
+const isActivityScheduledEvent = (
+  event: HistoryEventWithId,
+): event is ScheduledActivityEvent => {
+  return event.eventType === 'ActivityTaskScheduled';
+};
+
+export class Activity {
+  id: string;
+  name: string;
+  private _events: Map<ActivityType, HistoryEventWithId> = new Map();
+
+  constructor(event: ScheduledActivityEvent) {
+    this.id = event?.activityTaskScheduledEventAttributes?.activityId;
+    this.name = event?.activityTaskScheduledEventAttributes?.activityType?.name;
+
+    this.set(event.eventType, event);
+  }
+
+  set(type: ActivityType, event: HistoryEventWithId) {
+    this._events.set(type, event);
+  }
+
+  get(type: ActivityType): HistoryEventWithId {
+    return this._events.get(type);
+  }
+
+  toArray(): HistoryEventWithId[] {
+    return [...this];
+  }
+
+  get events(): typeof this._events {
+    return this._events;
+  }
+
+  get length(): number {
+    return this.events.size;
+  }
+
+  get last(): HistoryEventWithId {
+    let last: HistoryEventWithId;
+
+    for (const event of this) {
+      last = event;
+    }
+
+    return last;
+  }
+
+  [Symbol.iterator]() {
+    return this._events.values();
+  }
+}
+
+export class Activities {
+  private _activities: Map<string, Activity> = new Map();
+
+  static from = (
+    events: HistoryEventWithId[],
+    activities = new Activities(),
+  ): Activities => {
+    for (const event of events) {
+      if (isActivity(event)) {
+        activities.add(event);
+      }
+    }
+
+    return activities;
+  };
+
+  constructor(event?: ActivityEvent | HistoryEventWithId[]) {
+    if (Array.isArray(event)) return Activities.from(event, this);
+    if (event) this.add(event);
+  }
+
+  get(id: string | number | Long) {
+    return this._activities.get(String(id));
+  }
+
+  add(event: ActivityEvent) {
+    if (isActivityScheduledEvent(event)) {
+      const id = String(event.id);
+      return this._activities.set(id, new Activity(event));
+    }
+
+    if (event.eventType === 'ActivityTaskStarted') {
+      const { scheduledEventId } = event.activityTaskStartedEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+
+    if (event.eventType === 'ActivityTaskCanceled') {
+      const { scheduledEventId } = event.activityTaskCanceledEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+
+    if (event.eventType === 'ActivityTaskCancelRequested') {
+      const { scheduledEventId } =
+        event.activityTaskCancelRequestedEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+
+    if (event.eventType === 'ActivityTaskFailed') {
+      const { scheduledEventId } = event.activityTaskFailedEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+
+    if (event.eventType === 'ActivityTaskTimedOut') {
+      const { scheduledEventId } = event.activityTaskTimedOutEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+
+    if (event.eventType === 'ActivityTaskCompleted') {
+      const { scheduledEventId } = event.activityTaskCompletedEventAttributes;
+      return this.get(scheduledEventId).set(event.eventType, event);
+    }
+  }
+
+  get length(): number {
+    return this._activities.size;
+  }
+
+  slice(...args: Parameters<typeof Array.prototype.slice>): Activity[] {
+    return [...this].slice(...args);
+  }
+
+  [Symbol.iterator]() {
+    return this._activities.values();
+  }
+}
diff --git a/src/lib/services/cluster-service.ts b/src/lib/services/cluster-service.ts
new file mode 100644
index 00000000..7163a9a8
--- /dev/null
+++ b/src/lib/services/cluster-service.ts
@@ -0,0 +1,10 @@
+import { requestFromAPI } from '$lib/utilities/request-from-api';
+import type { GetClusterInfoResponse } from '$types';
+
+export const fetchCluster = async (
+  request = fetch,
+): Promise<GetClusterInfoResponse> => {
+  return await requestFromAPI('/cluster', {
+    request,
+  });
+};
diff --git a/src/lib/services/pollers-service.ts b/src/lib/services/pollers-service.ts
index a388f97d..1797a103 100644
--- a/src/lib/services/pollers-service.ts
+++ b/src/lib/services/pollers-service.ts
@@ -28,13 +28,13 @@ export async function getPollers(
   request = fetch,
 ): Promise<GetPollersResponse> {
   const workflowPollers = await requestFromAPI<GetPollersResponse>(
-    `/namespaces/${namespace}/task-queues/${queue}?task_queue_type=1`,
-    { request },
+    `/namespaces/${namespace}/task-queues/${queue}`,
+    { request, params: { taskQueueType: '1' } },
   );
 
   const activityPollers = await requestFromAPI<GetPollersResponse>(
-    `/namespaces/${namespace}/task-queues/${queue}?task_queue_type=2`,
-    { request },
+    `/namespaces/${namespace}/task-queues/${queue}`,
+    { request, params: { taskQueueType: '2' } },
   );
 
   activityPollers.pollers.forEach((poller: PollerWithTaskQueueTypes) => {
diff --git a/src/lib/services/settings-service.ts b/src/lib/services/settings-service.ts
new file mode 100644
index 00000000..96d075b4
--- /dev/null
+++ b/src/lib/services/settings-service.ts
@@ -0,0 +1,14 @@
+import { requestFromAPI } from '$lib/utilities/request-from-api';
+
+export const fetchSettings = async (request = fetch): Promise<Settings> => {
+  const settings: { Auth: { Enabled: boolean } } = await requestFromAPI(
+    `/settings`,
+    { request },
+  );
+
+  return {
+    auth: {
+      enabled: settings.Auth.Enabled,
+    },
+  };
+};
diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts
index 400c9e62..44a1c569 100644
--- a/src/lib/services/workflow-service.ts
+++ b/src/lib/services/workflow-service.ts
@@ -1,20 +1,21 @@
-import { sub, formatISO } from 'date-fns';
+import { get } from 'svelte/store';
+
+import type { ListWorkflowExecutionsResponse } from '$types';
+import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
 import { requestFromAPI } from '$lib/utilities/request-from-api';
+
 import {
   toWorkflowExecution,
   toWorkflowExecutions,
 } from '$lib/models/workflow-execution';
-import { toDuration } from '$lib/utilities/to-duration';
-
-import { fetchEvents } from './events-service';
 import { toEventHistory } from '$lib/models/event-history';
 
-import type { ListWorkflowExecutionsResponse } from '$types';
-import type { WorkflowExecution } from '$lib/models/workflow-execution';
-import { getStatusFilterCode } from '$lib/utilities/get-workflow-status-filter-code';
+import { fetchEvents } from './events-service';
 import { convertEventPayloadFromDataConverter } from './data-converter';
-import { get } from 'svelte/store';
+
 import { dataConverterPort } from '$lib/stores/data-converter-config';
+import { getWorkflowFilterParameters } from '$lib/utilities/get-workflow-filter-parameters';
 
 export type GetWorkflowExecutionRequest = NamespaceScopedRequest & {
   executionId: string;
@@ -26,40 +27,35 @@ type CombinedWorkflowExecutionsResponse = {
   nextPageTokens: NextPageTokens;
 };
 
-type TimeRangeParameter = {
-  timeRange?: Duration | string;
-};
-
-type StatusParameters = {
-  status?: WorkflowStatus;
-};
-
-const createDate = (timeRange: Duration | string) => {
-  const duration =
-    typeof timeRange === 'string' ? toDuration(timeRange) : timeRange;
-  return formatISO(sub(new Date(), duration));
-};
-
 const emptyWorkflowRequest = (): Promise<ListWorkflowExecutionsResponse> => {
   return Promise.resolve({
     executions: [],
   });
 };
 
+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,
-  { timeRange, status }: TimeRangeParameter & StatusParameters,
+  parameters: FilterParameters,
   request = fetch,
 ): Promise<ListWorkflowExecutionsResponse> => {
-  if (status && status !== 'Running') return emptyWorkflowRequest();
+  if (filterIsNotSetToRunning(parameters)) return emptyWorkflowRequest();
+
+  const params = getWorkflowFilterParameters(parameters);
+
   return requestFromAPI<ListWorkflowExecutionsResponse>(
     `/namespaces/${namespace}/workflows/open`,
     {
-      params: {
-        'start_time_filter.earliest_time': createDate(
-          timeRange || { hours: 24 },
-        ),
-      },
+      params,
       request,
     },
   );
@@ -67,18 +63,12 @@ export const fetchOpenWorkflows = (
 
 export const fetchClosedWorkflows = (
   namespace: string,
-  { timeRange, status }: TimeRangeParameter & StatusParameters,
+  parameters: FilterParameters,
   request = fetch,
 ): Promise<ListWorkflowExecutionsResponse> => {
-  if (status === 'Running') return emptyWorkflowRequest();
+  if (filterIsSetToRunning(parameters)) return emptyWorkflowRequest();
 
-  const params: Record<string, string> = {
-    'start_time_filter.earliest_time': createDate(timeRange || { hours: 24 }),
-  };
-
-  const statusFilter = getStatusFilterCode(status);
-
-  if (statusFilter) params['status_filter.status'] = statusFilter;
+  const params = getWorkflowFilterParameters(parameters);
 
   return requestFromAPI<ListWorkflowExecutionsResponse>(
     `/namespaces/${namespace}/workflows/closed`,
@@ -91,13 +81,14 @@ export const fetchClosedWorkflows = (
 
 export const fetchAllWorkflows = async (
   namespace: string,
-  { timeRange, status }: TimeRangeParameter & StatusParameters,
+  parameters: FilterParameters,
   request = fetch,
 ): Promise<CombinedWorkflowExecutionsResponse> => {
   const [open, closed] = await Promise.all([
-    fetchOpenWorkflows(namespace, { timeRange, status }, request),
-    fetchClosedWorkflows(namespace, { timeRange, status }, request),
+    fetchOpenWorkflows(namespace, parameters, request),
+    fetchClosedWorkflows(namespace, parameters, request),
   ]);
+
   const executions = [...open?.executions, ...closed?.executions];
 
   return {
diff --git a/src/lib/stores/banner.ts b/src/lib/stores/banner.ts
new file mode 100644
index 00000000..f4b4a8e7
--- /dev/null
+++ b/src/lib/stores/banner.ts
@@ -0,0 +1,7 @@
+import { persistStore } from '$lib/utilities/persist-store';
+
+export const closedBannerId = persistStore('closedBannerId', null);
+
+export const close = (bannerId: string): void => {
+  closedBannerId.set(bannerId);
+};
diff --git a/src/lib/stores/cluster.ts b/src/lib/stores/cluster.ts
new file mode 100644
index 00000000..10853a09
--- /dev/null
+++ b/src/lib/stores/cluster.ts
@@ -0,0 +1,13 @@
+import { writable } from 'svelte/store';
+
+import { fetchCluster } from '$lib/services/cluster-service';
+import type { GetClusterInfoResponse } from '$types';
+
+export const cluster = writable<GetClusterInfoResponse>({});
+
+const loadCluster = async (): Promise<void> => {
+  const clusterRes = await fetchCluster();
+  cluster.set(clusterRes);
+};
+
+loadCluster();
diff --git a/src/lib/stores/data-converter-config.ts b/src/lib/stores/data-converter-config.ts
index 4dc80bd3..d7bb91cb 100644
--- a/src/lib/stores/data-converter-config.ts
+++ b/src/lib/stores/data-converter-config.ts
@@ -1,7 +1,5 @@
-import { browser } from '$app/env';
 import { writable } from 'svelte/store';
-
-import type { Writable } from 'svelte/store';
+import { persistStore } from '$lib/utilities/persist-store';
 
 export const dataConverterPort = persistStore('port', null);
 
@@ -16,30 +14,3 @@ export function setLastDataConverterFailure(): void {
 export function setLastDataConverterSuccess(): void {
   lastDataConverterStatus.set('success');
 }
-
-export function persistStore(
-  name: string,
-  initialValue: string | null = '',
-): Pick<Writable<string>, 'subscribe' | 'set'> {
-  let initialStoreValue = initialValue;
-  if (browser) {
-    try {
-      initialStoreValue = JSON.parse(window?.localStorage?.getItem(name));
-    } catch (_err) {
-      initialStoreValue = null;
-    }
-  }
-
-  const { subscribe, set } = writable(initialStoreValue);
-
-  return {
-    subscribe,
-
-    set: (x: string | null) => {
-      if (browser) {
-        window?.localStorage?.setItem(name, JSON.stringify(x));
-      }
-      set(x);
-    },
-  };
-}
diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts
new file mode 100644
index 00000000..6cecbd17
--- /dev/null
+++ b/src/lib/stores/settings.ts
@@ -0,0 +1,16 @@
+import { writable } from 'svelte/store';
+
+import { fetchSettings } from '$lib/services/settings-service';
+
+export const settings = writable<Settings>({
+  auth: {
+    enabled: false,
+  },
+});
+
+const loadSettings = async (): Promise<void> => {
+  const settingsRes = await fetchSettings();
+  settings.set(settingsRes);
+};
+
+loadSettings();
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/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-component-for-event-type.ts b/src/lib/utilities/get-component-for-event-type.ts
deleted file mode 100644
index 7afd67bc..00000000
--- a/src/lib/utilities/get-component-for-event-type.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import WorkflowExecutionStartedEvent from '$lib/components/events/workflow-execution-started-event.svelte';
-import WorkflowExecutionCompletedEvent from '$lib/components/events/workflow-execution-completed-event.svelte';
-import WorkflowExecutionFailedEvent from '$lib/components/events/workflow-execution-failed-event.svelte';
-import WorkflowExecutionTimedOutEvent from '$lib/components/events/workflow-execution-timed-out-event.svelte';
-import WorkflowTaskScheduledEvent from '$lib/components/events/workflow-task-scheduled-event.svelte';
-import WorkflowTaskStartedEvent from '$lib/components/events/workflow-task-started-event.svelte';
-import WorkflowTaskCompletedEvent from '$lib/components/events/workflow-task-completed-event.svelte';
-import WorkflowTaskTimedOutEvent from '$lib/components/events/workflow-task-timed-out-event.svelte';
-import WorkflowTaskFailedEvent from '$lib/components/events/workflow-task-failed-event.svelte';
-import ActivityTaskScheduledEvent from '$lib/components/events/activity-task-scheduled-event.svelte';
-import ActivityTaskStartedEvent from '$lib/components/events/activity-task-started-event.svelte';
-import ActivityTaskCompletedEvent from '$lib/components/events/activity-task-completed-event.svelte';
-import ActivityTaskFailedEvent from '$lib/components/events/activity-task-failed-event.svelte';
-import ActivityTaskTimedOutEvent from '$lib/components/events/activity-task-timed-out-event.svelte';
-import TimerStartedEvent from '$lib/components/events/timer-started-event.svelte';
-import TimerFiredEvent from '$lib/components/events/timer-fired-event.svelte';
-import ActivityTaskCancelRequestedEvent from '$lib/components/events/activity-task-cancel-requested-event.svelte';
-import ActivityTaskCanceledEvent from '$lib/components/events/activity-task-canceled-event.svelte';
-import TimerCanceledEvent from '$lib/components/events/timer-canceled-event.svelte';
-import MarkerRecordedEvent from '$lib/components/events/marker-recorded-event.svelte';
-import WorkflowExecutionSignaledEvent from '$lib/components/events/workflow-execution-signaled-event.svelte';
-import WorkflowExecutionTerminatedEvent from '$lib/components/events/workflow-execution-terminated-event.svelte';
-import WorkflowExecutionCancelRequestedEvent from '$lib/components/events/workflow-execution-cancel-requested-event.svelte';
-import WorkflowExecutionCanceledEvent from '$lib/components/events/workflow-execution-canceled-event.svelte';
-import RequestCancelExternalWorkflowExecutionInitiatedEvent from '$lib/components/events/request-cancel-external-workflow-execution-initiated-event.svelte';
-import RequestCancelExternalWorkflowExecutionFailedEvent from '$lib/components/events/request-cancel-external-workflow-execution-failed-event.svelte';
-import ExternalWorkflowExecutionCancelRequestedEvent from '$lib/components/events/external-workflow-execution-cancel-requested-event.svelte';
-import WorkflowExecutionContinuedAsNewEvent from '$lib/components/events/workflow-execution-continued-as-new-event.svelte';
-import StartChildWorkflowExecutionInitiatedEvent from '$lib/components/events/start-child-workflow-execution-initiated-event.svelte';
-import StartChildWorkflowExecutionFailedEvent from '$lib/components/events/start-child-workflow-execution-failed-event.svelte';
-import ChildWorkflowExecutionStartedEvent from '$lib/components/events/child-workflow-execution-started-event.svelte';
-import ChildWorkflowExecutionCompletedEvent from '$lib/components/events/child-workflow-execution-completed-event.svelte';
-import ChildWorkflowExecutionFailedEvent from '$lib/components/events/child-workflow-execution-failed-event.svelte';
-import ChildWorkflowExecutionCanceledEvent from '$lib/components/events/child-workflow-execution-canceled-event.svelte';
-import ChildWorkflowExecutionTimedOutEvent from '$lib/components/events/child-workflow-execution-timed-out-event.svelte';
-import ChildWorkflowExecutionTerminatedEvent from '$lib/components/events/child-workflow-execution-terminated-event.svelte';
-import SignalExternalWorkflowExecutionInitiatedEvent from '$lib/components/events/signal-external-workflow-execution-initiated-event.svelte';
-import SignalExternalWorkflowExecutionFailedEvent from '$lib/components/events/signal-external-workflow-execution-failed-event.svelte';
-import ExternalWorkflowExecutionSignaledEvent from '$lib/components/events/external-workflow-execution-signaled-event.svelte';
-import UpsertWorkflowSearchAttributesEvent from '$lib/components/events/upsert-workflow-search-attributes-event.svelte';
-
-const eventTypes = {
-  WorkflowExecutionStarted: WorkflowExecutionStartedEvent,
-  WorkflowExecutionCompleted: WorkflowExecutionCompletedEvent,
-  WorkflowExecutionFailed: WorkflowExecutionFailedEvent,
-  WorkflowExecutionTimedOut: WorkflowExecutionTimedOutEvent,
-  WorkflowTaskScheduled: WorkflowTaskScheduledEvent,
-  WorkflowTaskStarted: WorkflowTaskStartedEvent,
-  WorkflowTaskCompleted: WorkflowTaskCompletedEvent,
-  WorkflowTaskTimedOut: WorkflowTaskTimedOutEvent,
-  WorkflowTaskFailed: WorkflowTaskFailedEvent,
-  ActivityTaskScheduled: ActivityTaskScheduledEvent,
-  ActivityTaskStarted: ActivityTaskStartedEvent,
-  ActivityTaskCompleted: ActivityTaskCompletedEvent,
-  ActivityTaskFailed: ActivityTaskFailedEvent,
-  ActivityTaskTimedOut: ActivityTaskTimedOutEvent,
-  TimerStarted: TimerStartedEvent,
-  TimerFired: TimerFiredEvent,
-  ActivityTaskCancelRequested: ActivityTaskCancelRequestedEvent,
-  ActivityTaskCanceled: ActivityTaskCanceledEvent,
-  TimerCanceled: TimerCanceledEvent,
-  MarkerRecorded: MarkerRecordedEvent,
-  WorkflowExecutionSignaled: WorkflowExecutionSignaledEvent,
-  WorkflowExecutionTerminated: WorkflowExecutionTerminatedEvent,
-  WorkflowExecutionCancelRequested: WorkflowExecutionCancelRequestedEvent,
-  WorkflowExecutionCanceled: WorkflowExecutionCanceledEvent,
-  RequestCancelExternalWorkflowExecutionInitiated:
-    RequestCancelExternalWorkflowExecutionInitiatedEvent,
-  RequestCancelExternalWorkflowExecutionFailed:
-    RequestCancelExternalWorkflowExecutionFailedEvent,
-  ExternalWorkflowExecutionCancelRequested:
-    ExternalWorkflowExecutionCancelRequestedEvent,
-  WorkflowExecutionContinuedAsNew: WorkflowExecutionContinuedAsNewEvent,
-  StartChildWorkflowExecutionInitiated:
-    StartChildWorkflowExecutionInitiatedEvent,
-  StartChildWorkflowExecutionFailed: StartChildWorkflowExecutionFailedEvent,
-  ChildWorkflowExecutionStarted: ChildWorkflowExecutionStartedEvent,
-  ChildWorkflowExecutionCompleted: ChildWorkflowExecutionCompletedEvent,
-  ChildWorkflowExecutionFailed: ChildWorkflowExecutionFailedEvent,
-  ChildWorkflowExecutionCanceled: ChildWorkflowExecutionCanceledEvent,
-  ChildWorkflowExecutionTimedOut: ChildWorkflowExecutionTimedOutEvent,
-  ChildWorkflowExecutionTerminated: ChildWorkflowExecutionTerminatedEvent,
-  SignalExternalWorkflowExecutionInitiated:
-    SignalExternalWorkflowExecutionInitiatedEvent,
-  SignalExternalWorkflowExecutionFailed:
-    SignalExternalWorkflowExecutionFailedEvent,
-  ExternalWorkflowExecutionSignaled: ExternalWorkflowExecutionSignaledEvent,
-  UpsertWorkflowSearchAttributes: UpsertWorkflowSearchAttributesEvent,
-};
-
-type EventTypes = typeof eventTypes;
-
-export const getComponentForEventType = (
-  event: HistoryEvent,
-): EventTypes[keyof EventTypes] => {
-  return eventTypes[event.eventType];
-};
diff --git a/src/lib/utilities/get-event-classification.ts b/src/lib/utilities/get-event-classification.ts
index c78e4a8d..8d786ff5 100644
--- a/src/lib/utilities/get-event-classification.ts
+++ b/src/lib/utilities/get-event-classification.ts
@@ -1,6 +1,25 @@
+import { Activity } from '../models/activity';
+import { format } from './format-camel-case';
+import { routeFor } from './route-for';
+import type { EventParameter } from './route-for';
+
 export type EventClassification = typeof eventClassifications[number];
+type EventOrActivity = HistoryEventWithId | PendingActivity | Activity;
+
+type EventSummary = {
+  id: string;
+  name: string;
+  timeStamp: string;
+  classification: EventClassification;
+  tag: string;
+  pending: boolean;
+  activity: boolean;
+  type: 'event' | 'pending-activity' | 'activity';
+  routeFor: (parameters: EventParameter) => string;
+};
 
 export const eventClassifications = [
+  'Unspecified',
   'Scheduled',
   'Open',
   'New',
@@ -17,13 +36,96 @@ 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 isPendingActivity = (
+  event: EventOrActivity,
+): 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 isActivity = (event: EventOrActivity): event is Activity => {
+  return event instanceof Activity;
+};
+
 export const getEventClassification = (
-  event: HistoryEventWithId,
+  event: EventOrActivity,
 ): EventClassification => {
-  const eventType = event.eventType.toString();
+  if (isPendingActivity(event)) return event.state;
+  if (isActivity(event)) event = event.get('ActivityTaskScheduled');
+
+  const eventType = event.eventType;
 
   if (eventType.includes('RequestCancel')) return 'CancelRequested';
   for (const classification of eventClassifications) {
     if (eventType.includes(classification)) return classification;
   }
 };
+
+const getName = (event: EventOrActivity): string => {
+  if (isEvent(event)) return String(event.eventType);
+  if (isPendingActivity(event))
+    return `${event.activityType.name}:${event.state}`;
+  if (isActivity(event)) return event.name;
+};
+
+const getTime = (event: EventOrActivity): string => {
+  if (isEvent(event)) return String(event.eventTime);
+  if (isPendingActivity(event)) return String(event.lastStartedTime);
+  if (isActivity(event)) return String(event.last.eventTime);
+};
+
+const getId = (event: EventOrActivity): string => {
+  if (isEvent(event)) return String(event.eventId);
+  if (isPendingActivity(event)) return String(event.activityId);
+  if (isActivity(event)) return String(event.id);
+};
+
+const getType = (
+  event: EventOrActivity,
+): 'event' | 'pending-activity' | 'activity' => {
+  if (isEvent(event)) return 'event';
+  if (isPendingActivity(event)) return 'pending-activity';
+  if (isActivity(event)) return 'activity';
+};
+
+export const getHref = (
+  event: EventOrActivity,
+  parameters: EventParameter,
+): string => {
+  if (isEvent(event)) return routeFor('workflow.events.full.event', parameters);
+  if (isPendingActivity(event))
+    routeFor('workflow.events.full.pending', parameters);
+  if (isActivity(event))
+    return routeFor('workflow.events.compact.activity', parameters);
+};
+
+export const formatEvent = (
+  event: EventOrActivity | Activity,
+): 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: isActivity(event),
+    routeFor: (parameters: EventParameter) => getHref(event, parameters),
+  };
+};
diff --git a/src/lib/utilities/get-workflow-execution-url.ts b/src/lib/utilities/get-workflow-execution-url.ts
deleted file mode 100644
index 1ef865c2..00000000
--- a/src/lib/utilities/get-workflow-execution-url.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { WorkflowExecution } from '$lib/models/workflow-execution';
-
-type WorkflowIdentifier = Pick<WorkflowExecution, 'id' | 'runId'>;
-
-export const getWorkflowExecutionUrl = (
-  namespace: string,
-  workflow: WorkflowIdentifier,
-): string => {
-  if (!workflow) return;
-  return `/namespaces/${namespace}/workflows/${workflow.id}/${workflow.runId}`;
-};
diff --git a/src/lib/utilities/get-workflow-filter-parameters.ts b/src/lib/utilities/get-workflow-filter-parameters.ts
new file mode 100644
index 00000000..7fe0e70a
--- /dev/null
+++ b/src/lib/utilities/get-workflow-filter-parameters.ts
@@ -0,0 +1,31 @@
+import { formatISO, sub } from 'date-fns';
+
+import { toDuration } from './to-duration';
+import { getStatusFilterCode } from './get-workflow-status-filter-code';
+
+const createDate = (timeRange: Duration | string) => {
+  const duration =
+    typeof timeRange === 'string' ? toDuration(timeRange) : timeRange;
+  return formatISO(sub(new Date(), duration));
+};
+
+export const getWorkflowFilterParameters = ({
+  workflowId,
+  workflowType,
+  timeRange,
+  status,
+}: FilterParameters = {}): Record<string, string> => {
+  const params: Record<string, string> = {};
+  const statusFilter = getStatusFilterCode(status);
+
+  params['start_time_filter.earliest_time'] = createDate(
+    timeRange || { hours: 24 },
+  );
+
+  if (workflowId) params['execution_filter.workflow_id'] = workflowId;
+  if (workflowType) params['type_filter.name'] = workflowType;
+
+  if (statusFilter) params['status_filter.status'] = statusFilter;
+
+  return params;
+};
diff --git a/src/lib/utilities/persist-store.ts b/src/lib/utilities/persist-store.ts
new file mode 100644
index 00000000..63bf14f8
--- /dev/null
+++ b/src/lib/utilities/persist-store.ts
@@ -0,0 +1,31 @@
+import { browser } from '$app/env';
+import { writable } from 'svelte/store';
+
+import type { Writable } from 'svelte/store';
+
+export function persistStore(
+  name: string,
+  initialValue: string | null = '',
+): Pick<Writable<string>, 'subscribe' | 'set'> {
+  let initialStoreValue = initialValue;
+  if (browser) {
+    try {
+      initialStoreValue = JSON.parse(window?.localStorage?.getItem(name));
+    } catch (_err) {
+      initialStoreValue = null;
+    }
+  }
+
+  const { subscribe, set } = writable(initialStoreValue);
+
+  return {
+    subscribe,
+
+    set: (x: string | null) => {
+      if (browser) {
+        window?.localStorage?.setItem(name, JSON.stringify(x));
+      }
+      set(x);
+    },
+  };
+}
diff --git a/src/lib/utilities/request-from-api.ts b/src/lib/utilities/request-from-api.ts
index adbf347e..e5f9d8e2 100644
--- a/src/lib/utilities/request-from-api.ts
+++ b/src/lib/utilities/request-from-api.ts
@@ -55,7 +55,6 @@ export const requestFromAPI = async <T>(
   });
 
   const url = toURL(base + '/api/v1' + encode(endpoint), query);
-
   try {
     const response = await request(url, options);
 
diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts
new file mode 100644
index 00000000..3841f7be
--- /dev/null
+++ b/src/lib/utilities/route-for.ts
@@ -0,0 +1,196 @@
+type RoutePath =
+  | 'workflows'
+  | 'workflow'
+  | 'workflow.events'
+  | '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 NamespaceParameter = { namespace: string };
+export type WorkflowParameters = {
+  workflowId: string;
+  runId: string;
+} & NamespaceParameter;
+export type EventParameter = {
+  eventId: string;
+} & WorkflowParameters;
+export type ActivityParameter = {
+  activityId: string;
+} & EventParameter;
+export type TaskQueueParameter = {
+  queue: string;
+} & NamespaceParameter;
+
+export type RouteParameters = NamespaceParameter &
+  WorkflowParameters &
+  EventParameter &
+  ActivityParameter &
+  TaskQueueParameter;
+
+const routeForNamespace = ({ namespace }: NamespaceParameter): string => {
+  return `/namespaces/${namespace}`;
+};
+
+const routeForWorkflows = (parameters: NamespaceParameter): string => {
+  return `${routeForNamespace(parameters)}/workflows`;
+};
+
+const routeForWorkflow = ({
+  workflowId,
+  runId,
+  ...parameters
+}: WorkflowParameters): string => {
+  return `${routeForWorkflows(parameters)}/${workflowId}/${runId}`;
+};
+
+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,
+  eventId: string,
+): string => {
+  return `${routeForEventHistory(parameters, view)}/${eventType}-${eventId}`;
+};
+
+const routeForActivity = (
+  parameters: WorkflowParameters,
+  view: EventHistoryView,
+  eventType: EventView,
+  activityId: string,
+  eventId: string,
+): string => {
+  return `${routeForEventHistory(
+    parameters,
+    view,
+  )}/${eventType}-${eventId}/events/${activityId}`;
+};
+
+const routeForWorkers = ({ queue, ...parameters }: TaskQueueParameter) => {
+  return `${routeForNamespace(parameters)}/workers/${queue}`;
+};
+
+export function routeFor(
+  path: 'workflows',
+  parameters: NamespaceParameter,
+): string;
+export function routeFor(
+  path:
+    | 'workflow'
+    | 'workflow.events'
+    | 'workflow.events.full'
+    | 'workflow.events.compact'
+    | 'workflow.events.json'
+    | 'workflow.stack-trace'
+    | 'workflow.query',
+  parameters: WorkflowParameters,
+): string;
+export function routeFor(
+  path:
+    | 'workflow.events.full.event'
+    | 'workflow.events.full.pending'
+    | 'workflow.events.compact.activity',
+  parameters: EventParameter,
+): string;
+export function routeFor(
+  path: 'workflow.events.compact.activity.event',
+  parameters: ActivityParameter,
+): string;
+export function routeFor(
+  path: 'workers',
+  parameters: TaskQueueParameter,
+): string;
+export function routeFor(path: RoutePath, parameters: RouteParameters): string {
+  if (path === 'workflows') {
+    return routeForWorkflows(parameters);
+  }
+
+  if (path === 'workflow') {
+    return routeForWorkflow(parameters);
+  }
+
+  if (path === 'workflow.events') {
+    return routeForEventHistory(parameters);
+  }
+
+  if (path === 'workflow.events.full') {
+    return routeForEventHistory(parameters, 'full');
+  }
+
+  if (path === 'workflow.events.compact') {
+    return routeForEventHistory(parameters, 'compact');
+  }
+
+  if (path === 'workflow.events.json') {
+    return routeForEventHistory(parameters, 'json');
+  }
+
+  if (path === 'workflow.events.full.event') {
+    return routeForEventHistoryItem(
+      parameters,
+      'full',
+      'event',
+      parameters.eventId,
+    );
+  }
+
+  if (path === 'workflow.events.full.pending') {
+    return routeForEventHistoryItem(
+      parameters,
+      'full',
+      'pending',
+      parameters.eventId,
+    );
+  }
+
+  if (path === 'workflow.events.compact.activity') {
+    return routeForEventHistoryItem(
+      parameters,
+      'compact',
+      'activity',
+      parameters.eventId,
+    );
+  }
+
+  if (path === 'workflow.events.compact.activity.event') {
+    return routeForActivity(
+      parameters,
+      'compact',
+      'activity',
+      parameters.eventId,
+      parameters.activityId,
+    );
+  }
+
+  if (path === 'workflow.query') {
+    return routeForWorkflow(parameters) + '/query';
+  }
+
+  if (path === 'workflow.stack-trace') {
+    return routeForWorkflow(parameters) + '/stack-trace';
+  }
+
+  if (path === 'workers') {
+    return routeForWorkers(parameters);
+  }
+}
diff --git a/src/lib/utilities/update-query-parameters.ts b/src/lib/utilities/update-query-parameters.ts
index 66387855..54ecaea8 100644
--- a/src/lib/utilities/update-query-parameters.ts
+++ b/src/lib/utilities/update-query-parameters.ts
@@ -1,25 +1,31 @@
 import type { goto, invalidate } from '$app/navigation';
+import { appendQueryParameters } from './append-query-parameters';
 
 type UpdateQueryParams = {
   parameter: string;
-  value: string;
+  value?: string;
   query: URLSearchParams;
   path: string;
   goto: typeof goto;
   invalidate?: typeof invalidate;
 };
 
-export const updateQueryParameters = ({
+const options = { replaceState: true, keepfocus: true };
+
+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);
   }
-  goto(`${path}?${query}`, { replaceState: true, keepfocus: true });
+
+  goto(appendQueryParameters(path, query), options);
+
+  return value;
 };
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index 46865105..78678c90 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -26,6 +26,7 @@
 
   import Header from './_header.svelte';
   import Notifications from '$lib/components/notifications.svelte';
+  import Banner from '$lib/components/banner.svelte';
 
   export let namespaces: DescribeNamespaceResponse[];
 
@@ -44,10 +45,13 @@
   <meta property="og:image" content="/banner.png" />
 </svelte:head>
 
-<main class="h-screen overflow-y-hidden">
+<main class="flex flex-col h-screen">
   <Notifications />
+  <Banner />
   <Header />
-  <section id="content" class="h-full mx-10">
-    <slot />
+  <section id="content" class="h-full mx-10 mb-10 mt-8">
+    <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 67c6605a..1817f249 100644
--- a/src/routes/_header.svelte
+++ b/src/routes/_header.svelte
@@ -16,37 +16,58 @@
 <script lang="ts">
   import { namespace } from '$lib/stores/namespace';
   import DataConvertorStatus from '$lib/components/data-convertor-status.svelte';
+  import { settings } from '$lib/stores/settings';
+  import NavigationLink from './_navigation-link.svelte';
   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 border-b-2"
+<header
+  class="grid grid-rows-1 grid-cols-12 px-10 items-center bg-gray-900 shadow-lg"
 >
-  <div class="w-40">
+  <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>
-  <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}
-      <span href={`/namespaces/${$namespace}/settings`}>
-        {user.email}
-      </span>
-    {:else}
-      <a href={import.meta.env.VITE_API + '/auth/sso'}> Sign In </a>
+    {#if $settings.auth?.enabled}
+      {#if user.email}
+        <span href={`/namespaces/${$namespace}/settings`}>
+          {user.email}
+        </span>
+      {:else}
+        <a class="header-button" href={import.meta.env.VITE_API + '/auth/sso'}>
+          Sign In
+        </a>
+      {/if}
     {/if}
   </div>
-</div>
+</header>
 
 <style lang="postcss">
-  a {
-    @apply block px-4 py-2 text-sm;
+  .header-button {
+    @apply block px-4 py-2 text-sm text-white;
   }
 
-  a:hover {
-    @apply bg-purple-100 rounded-md;
+  .header-button:hover {
+    @apply bg-purple-100 rounded-md text-gray-900;
   }
 </style>
diff --git a/src/routes/_navigation-link.svelte b/src/routes/_navigation-link.svelte
new file mode 100644
index 00000000..4856d623
--- /dev/null
+++ b/src/routes/_navigation-link.svelte
@@ -0,0 +1,30 @@
+<script lang="ts">
+  import { page } from '$app/stores';
+  import { pathMatches } from '$lib/utilities/path-matches';
+
+  export let href: string;
+
+  $: isActive = pathMatches(href, $page.path);
+</script>
+
+<a {href} class:active={isActive} class="relative">
+  <slot />
+</a>
+
+<style lang="postcss">
+  a.active {
+    @apply text-blue-200 opacity-100;
+  }
+
+  a.active:after {
+    @apply absolute bg-blue-200 inset-x-0 bottom-0 rounded-t;
+    content: '';
+    height: 3px;
+  }
+  a:hover {
+    @apply opacity-100;
+  }
+  a {
+    @apply block px-2 py-5 text-base font-medium text-white;
+  }
+</style>
diff --git a/src/routes/cluster/index.svelte b/src/routes/cluster/index.svelte
index 890d9ccd..2214c20f 100644
--- a/src/routes/cluster/index.svelte
+++ b/src/routes/cluster/index.svelte
@@ -1,25 +1,8 @@
-<script context="module" lang="ts">
-  import type { GetClusterInfoResponse } from '$types';
-
-  import type { LoadInput } from '@sveltejs/kit';
-  import { requestFromAPI } from '$lib/utilities/request-from-api';
-
-  export async function load({ fetch }: LoadInput) {
-    const cluster: GetClusterInfoResponse = await requestFromAPI('/cluster', {
-      request: fetch,
-    });
-
-    return {
-      props: { cluster },
-    };
-  }
-</script>
-
 <script lang="ts">
   import KeyValueTable from '$lib/components/key-value-table.svelte';
+  import { cluster } from '$lib/stores/cluster';
 
-  export let cluster: GetClusterInfoResponse;
-  const { supportedClients, ...clusterInformation } = cluster;
+  const { supportedClients, ...clusterInformation } = $cluster;
 </script>
 
 <main class="flex flex-wrap">
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 80%
rename from src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-row.svelte
rename to src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-row.svelte
index 8cf59c2c..f3292052 100644
--- a/src/routes/namespaces/[namespace]/queues/[queue]/_queue-pollers-row.svelte
+++ b/src/routes/namespaces/[namespace]/queue/[queue]/_queue-pollers-row.svelte
@@ -1,7 +1,4 @@
 <script lang="ts">
-  import Icon from 'svelte-hero-icons/Icon.svelte';
-  import { Check } from 'svelte-hero-icons';
-
   import type { PollerWithTaskQueueTypes } from '$lib/services/pollers-service';
   import { formatDate } from '$lib/utilities/format-date';
 
@@ -19,14 +16,14 @@
   </td>
   {#if poller.taskQueueTypes.includes('WORKFLOW')}
     <td>
-      <Icon src={Check} class="w-8 h-8 pl-3" />
+      <i class="fas fa-check" />
     </td>
   {:else}
     <td />
   {/if}
   {#if poller.taskQueueTypes.includes('ACTIVITY')}
     <td>
-      <Icon src={Check} class="w-8 h-8 pl-3" />
+      <i class="fas fa-check" />
     </td>
   {:else}
     <td />
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]/report/__layout.svelte b/src/routes/namespaces/[namespace]/report/__layout.svelte
deleted file mode 100644
index 4a5c6f5e..00000000
--- a/src/routes/namespaces/[namespace]/report/__layout.svelte
+++ /dev/null
@@ -1,142 +0,0 @@
-<script lang="ts">
-  import Icon from 'svelte-hero-icons/Icon.svelte';
-  import {
-    Clock,
-    ArrowNarrowRight,
-    CursorClick,
-    ViewBoards,
-    ViewGrid,
-  } from 'svelte-hero-icons';
-</script>
-
-<section class="h-screen flex items-start flex-col justify-between">
-  <div class="w-full docs">
-    <div class="flex flex-row justify-around">
-      <div class="w-50 p-10">
-        <div
-          class="bg-blue-700 w-12 h-12 rounded flex items-center justify-center mb-3"
-        >
-          <Icon src={Clock} class="text-white w-7 h-7" />
-        </div>
-
-        <h3 class="font-medium mb-2">Schedules</h3>
-        <p class="font-light text-gray-500 mb-3">
-          Schedule Workflows to run periodically, at some point in the future,
-          or right now.
-        </p>
-        <div class="flex flex-col">
-          <a
-            href="https://docs.temporal.io/docs/content/what-is-a-schedule-to-start-timeout/"
-            class="text-purple-600 mb-4"
-            >View Details
-            <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            />
-          </a>
-          <a
-            href="https://docs.temporal.io/docs/content/what-is-a-schedule-to-start-timeout/"
-            class="text-purple-600"
-            >View Docs <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            /></a
-          >
-        </div>
-      </div>
-
-      <div class="w-50 p-10">
-        <div
-          class="bg-blue-700 w-12 h-12 rounded flex items-center justify-center mb-3"
-        >
-          <Icon src={CursorClick} class="text-white w-7 h-7" />
-        </div>
-
-        <h3 class="font-medium mb-2">Workflows</h3>
-        <p class="font-light text-gray-500 mb-3">
-          Execute Activities, sleep, and await Timers, and persist every step.
-        </p>
-        <div class="flex flex-col">
-          <a
-            href="https://docs.temporal.io/docs/concepts/workflows/"
-            class="text-purple-600 mb-4"
-            >View Details <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            />
-          </a>
-          <a
-            href="https://docs.temporal.io/docs/concepts/workflows/"
-            class="text-purple-600"
-            >View Docs <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            /></a
-          >
-        </div>
-      </div>
-      <div class="w-50 p-10">
-        <div
-          class="bg-blue-700 w-12 h-12 rounded flex items-center justify-center mb-3"
-        >
-          <Icon src={ViewBoards} class="text-white w-7 h-7" />
-        </div>
-
-        <h3 class="font-medium mb-2">Task Queues</h3>
-        <p class="font-light text-gray-500 mb-3">
-          Everything passes through here.
-        </p>
-        <div class="flex flex-col">
-          <a
-            href="https://docs.temporal.io/docs/concepts/task-queues/"
-            class="text-purple-600 mb-4"
-            >View Details <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            />
-          </a>
-          <a
-            href="https://docs.temporal.io/docs/concepts/task-queues/"
-            class="text-purple-600"
-            >View Docs <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            /></a
-          >
-        </div>
-      </div>
-      <div class="w-50 p-10">
-        <div
-          class="bg-blue-700 w-12 h-12 rounded flex items-center justify-center mb-3"
-        >
-          <Icon src={ViewGrid} class="text-white w-8 h-8" />
-        </div>
-
-        <h3 class="font-medium mb-2">Workers</h3>
-        <p class="font-light text-gray-500 mb-3">
-          Nothing happens without Workers listening to Task Queues.
-        </p>
-        <div class="flex flex-col">
-          <a
-            href="https://docs.temporal.io/docs/go/workers/"
-            class="text-purple-600 mb-4"
-            >View Details <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            />
-          </a>
-          <a
-            href="https://docs.temporal.io/docs/go/workers/"
-            class="text-purple-600"
-            >View Docs <Icon
-              src={ArrowNarrowRight}
-              class="inline text-purple-600 w-4 h-4"
-            /></a
-          >
-        </div>
-      </div>
-
-      <slot />
-    </div>
-  </div>
-</section>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
index 2cfe32c0..214a7ee6 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
@@ -25,9 +25,7 @@
   export let namespace: string;
 </script>
 
-<section class="border-l-2 h-screen">
-  <main class="w-full">
-    <Header {workflow} {namespace} />
-    <slot />
-  </main>
-</section>
+<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 e5d9f6e5..08999b0d 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
@@ -1,7 +1,8 @@
 <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 { ArrowLeft } from 'svelte-hero-icons';
-  import Icon from 'svelte-hero-icons/Icon.svelte';
   import WorkflowStatus from '$lib/components/workflow-status.svelte';
   import TerminateWorkflow from '$lib/components/terminate-workflow.svelte';
   import Tabs from './_tabs.svelte';
@@ -10,33 +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`}>
-          <Icon
-            src={ArrowLeft}
-            class={`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..a214ae5d 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
@@ -2,37 +2,29 @@
   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';
+  import { routeFor } from '$lib/utilities/route-for';
 
-  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: workflowId, run: runId } = $page.params;
   const historyEvents = workflow?.historyEvents;
 </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={routeFor('workflow.events.full', { namespace, workflowId, runId })}
+    amount={historyEvents}
+  />
+  <Tab label="Workers" href="#not-implemented" />
+  <Tab
+    label="Stack Trace"
+    href={routeFor('workflow.stack-trace', { namespace, workflowId, runId })}
+  />
+  <Tab
+    label="Query"
+    href={routeFor('workflow.query', { namespace, workflowId, runId })}
+  />
 </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]/events/_events-filters.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/events/_events-filters.svelte
deleted file mode 100644
index 037c4a7a..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/events/_events-filters.svelte
+++ /dev/null
@@ -1,54 +0,0 @@
-<script lang="ts">
-  import Icon from 'svelte-hero-icons/Icon.svelte';
-  import { Download } from 'svelte-hero-icons';
-  import type { Writable } from 'svelte/store';
-
-  import { convertToJSON } from '$lib/utilities/convert-to-json';
-  import Input from '$lib/components/filter-input.svelte';
-  import Button from '$lib/components/button.svelte';
-  export let events: BaseEvent[];
-  export let eventFormat: Writable<EventFormat>;
-  export let eventType: Writable<string>;
-  export let execution: string;
-
-  function setFormat(format: EventFormat) {
-    $eventFormat = format;
-  }
-
-  function clear() {
-    $eventType = null;
-  }
-
-  $: dataUri = convertToJSON(events);
-</script>
-
-<section class="p-4 flex gap-2 items-center justify-between">
-  <div>
-    <label for="format">View Format</label>
-    <Button on:click={() => setFormat('grid')}>GRID</Button>
-    <Button on:click={() => setFormat('json')}>JSON</Button>
-  </div>
-  <div class="flex justify-center items-end">
-    <Input
-      name="Event History"
-      id="filter-by-event-history"
-      bind:value={$eventType}
-    />
-    <Button on:click={clear}>Clear</Button>
-  </div>
-  <div>
-    <a
-      class="text-black-500 font-bold uppercase px-3 py-1 text-xs flex"
-      href={dataUri}
-      download={`${execution}.json`}
-    >
-      <Icon src={Download} class="text-black w-4 h-4" />export</a
-    >
-  </div>
-</section>
-
-<style lang="postcss">
-  label {
-    font-size: 14px;
-  }
-</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..66f457b4
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
@@ -0,0 +1,54 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+  import type { WorkflowParameters } from '$lib/utilities/route-for';
+
+  export async function load({ page }: LoadInput) {
+    const { workflow: workflowId, run: runId, namespace } = page.params;
+
+    return {
+      props: {
+        workflow: { workflowId, runId, namespace },
+      },
+    };
+  }
+</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';
+  import { routeFor } from '$lib/utilities/route-for';
+
+  export let workflow: WorkflowParameters;
+</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={routeFor('workflow.events.full', workflow)}
+        />
+        <ToggleButton
+          icon={faLayerGroup}
+          href={routeFor('workflow.events.compact', workflow)}
+        />
+        <ToggleButton
+          icon={faCode}
+          href={routeFor('workflow.events.json', workflow)}
+        />
+      </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..ca6a5066
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
@@ -0,0 +1,30 @@
+<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 { Activities } from '$lib/models/activity';
+  import EventTable from '$lib/components/event-table.svelte';
+
+  export let events: HistoryEventWithId[];
+
+  let activities = new Activities(events);
+</script>
+
+<EventTable events={activities}>
+  <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
new file mode 100644
index 00000000..1cce372a
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/__layout.svelte
@@ -0,0 +1,69 @@
+<script context="module" lang="ts">
+  import type { EventParameter } from '$lib/utilities/route-for';
+  import type { LoadInput } from '@sveltejs/kit';
+
+  export async function load({ page, stuff }: LoadInput) {
+    const {
+      workflow: workflowId,
+      run: runId,
+      namespace,
+      eventId,
+    } = page.params;
+
+    const { events } = stuff as {
+      events: HistoryEventWithId[];
+    };
+
+    return {
+      props: {
+        events,
+        params: {
+          workflowId,
+          runId,
+          namespace,
+          eventId,
+        },
+      },
+    };
+  }
+</script>
+
+<script lang="ts">
+  import { Activities } from '$lib/models/activity';
+  import { routeFor } from '$lib/utilities/route-for';
+  import { page } from '$app/stores';
+
+  export let events: HistoryEventWithId[];
+  export let params: EventParameter;
+
+  let activities = new Activities(events);
+  let activity = activities.get(params.eventId);
+
+  const getHref = (event: HistoryEventWithId) =>
+    routeFor('workflow.events.compact.activity.event', {
+      ...params,
+      activityId: activity.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 activity.toArray() as event}
+        <li>
+          <a href={getHref(event)} class:active={$page.path === getHref(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
new file mode 100644
index 00000000..5cc6c9fb
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/events/[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/compact/activity-[eventId]/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/index.svelte
new file mode 100644
index 00000000..cce3afc1
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/activity-[eventId]/index.svelte
@@ -0,0 +1,23 @@
+<script context="module" lang="ts">
+  import { routeFor } from '$lib/utilities/route-for';
+
+  export async function load({ page }) {
+    const {
+      workflow: workflowId,
+      run: runId,
+      namespace,
+      eventId,
+    } = page.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
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..535c8087
--- /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 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
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..c6a74701
--- /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 }: 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 a43421b0..7e4b29e3 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
@@ -1,60 +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 { 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';
-
-  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>
-    <div>
-      <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>
-    {#each visibleEvents as event}
-      <Event {event} />
-    {/each}
-  </section>
-</div>
diff --git a/src/routes/namespaces/[namespace]/report/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/queries.svelte
similarity index 100%
rename from src/routes/namespaces/[namespace]/report/index.svelte
rename to src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/queries.svelte
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..948770b4
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte
@@ -0,0 +1,36 @@
+<script context="module" lang="ts">
+  import type { LoadInput } from '@sveltejs/kit';
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
+  import { getPollers } from '$lib/services/pollers-service';
+  export async function load({ fetch, page, stuff }: LoadInput) {
+    const { namespace } = page.params;
+    const { workflow } = stuff as {
+      workflow: WorkflowExecution;
+    };
+    return await getPollers(
+      { queue: workflow.taskQueue, namespace },
+      fetch,
+    ).then((pollers) => ({
+      props: { pollers },
+    }));
+  }
+</script>
+
+<script lang="ts">
+  import WorkersTable from '$lib/components/workers-table.svelte';
+  import WorkersRow from '$lib/components/workers-row.svelte';
+  import EmptyState from '$lib/components/empty-state.svelte';
+
+  export let pollers;
+</script>
+
+<section>
+  {#each pollers.pollers as poller}
+    <WorkersTable>
+      <WorkersRow {poller} />
+    </WorkersTable>
+  {:else}
+    <EmptyState title={'No Workers Found'} />
+  {/each}
+</section>
diff --git a/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte b/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
index bfb87d15..e959582f 100644
--- a/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/_workflow-filters.svelte
@@ -6,6 +6,9 @@
 
   export let timeFormat: string = 'relative';
 
+  let workflowIdFilter = '';
+  let workflowTypeFilter = '';
+
   const durations = [
     '10 minutes',
     '60 minutes',
@@ -20,7 +23,7 @@
   const statuses = {
     All: null,
     Running: 'Running',
-    TimedOut: 'Timed Out',
+    'Timed Out': 'TimedOut',
     Completed: 'Completed',
     Failed: 'Failed',
     'Continued as New': 'ContinuedAsNew',
@@ -29,25 +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 id="workflow-id-filter" name="Workflow ID" value="" />
-    <FilterInput id="workflow-type-filter" name="Workflow Type" value="" />
-    <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-empty.svelte b/src/routes/namespaces/[namespace]/workflows/_workflows-empty.svelte
deleted file mode 100644
index b430d752..00000000
--- a/src/routes/namespaces/[namespace]/workflows/_workflows-empty.svelte
+++ /dev/null
@@ -1,29 +0,0 @@
-<script lang="ts">
-  import Button from '$lib/components/button.svelte';
-</script>
-
-<div class="flex flex-col justify-start items-center gap-2 empty">
-  <span class="flex rounded-full circle items-center justify-center">🔭</span>
-  <h2 class="font-medium">No Workflows Found</h2>
-  <p class="text">
-    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus
-    dignissim lectus, tempor lacinia turpis.
-  </p>
-  <Button as="a" href="https://docs.temporal.io/">📄 Go to Docs</Button>
-</div>
-
-<style lang="postcss">
-  .empty {
-    margin-top: 5rem;
-  }
-
-  .text {
-    width: 44%;
-  }
-
-  .circle {
-    width: 3rem;
-    height: 3rem;
-    background-color: #e1e0e7;
-  }
-</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/_workflows-summary-row.svelte b/src/routes/namespaces/[namespace]/workflows/_workflows-summary-row.svelte
index b4a69485..0cb83e3d 100644
--- a/src/routes/namespaces/[namespace]/workflows/_workflows-summary-row.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/_workflows-summary-row.svelte
@@ -1,15 +1,19 @@
 <script lang="ts">
   import type { WorkflowExecution } from '$lib/models/workflow-execution';
-
   import { namespace } from '$lib/stores/namespace';
-  import { getWorkflowExecutionUrl } from '$lib/utilities/get-workflow-execution-url';
+
   import Time from '$lib/components/workflow-time.svelte';
   import WorkflowStatus from '$lib/components/workflow-status.svelte';
+  import { routeFor } from '$lib/utilities/route-for';
 
   export let workflow: WorkflowExecution;
   export let timeFormat: string;
 
-  $: href = getWorkflowExecutionUrl($namespace, workflow);
+  $: href = routeFor('workflow', {
+    namespace: $namespace,
+    workflowId: workflow.id,
+    runId: workflow.runId,
+  });
 </script>
 
 <a sveltekit:noscroll {href}>
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 664d64b9..fb3c3988 100644
--- a/src/routes/namespaces/[namespace]/workflows/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/index.svelte
@@ -5,12 +5,14 @@
     if (!page.query.has('time-range')) page.query.set('time-range', '24 hours');
 
     const namespace = page.params.namespace;
+    const workflowId = page.query.get('workflow-id');
+    const workflowType = page.query.get('workflow-type');
     const timeRange = page.query.get('time-range');
     const status = page.query.get('status') as WorkflowStatus;
 
     const initialData = await fetchAllWorkflows(
-      page.params.namespace,
-      { timeRange, status },
+      namespace,
+      { workflowId, workflowType, timeRange, status },
       fetch,
     );
 
@@ -27,7 +29,7 @@
   import WorkflowsSummaryTable from './_workflows-summary-table.svelte';
   import WorkflowsSummaryRow from './_workflows-summary-row.svelte';
   import WorkflowFilters from './_workflow-filters.svelte';
-  import WorkflowsEmptyState from './_workflows-empty.svelte';
+  import EmptyState from '$lib/components/empty-state.svelte';
   import WorkflowsLoadingState from './_workflows-loading.svelte';
   import VirtualList from '@sveltejs/svelte-virtual-list';
 
@@ -40,21 +42,21 @@
   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}
+    <EmptyState
+      title={'No Workflows Found'}
+      content={'If you have filters applied, try adjusting them.'}
+    />
+  {/if}
+{/await}
diff --git a/src/types/index.ts b/src/types/index.ts
index dca6614e..efbaae88 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -106,6 +106,7 @@ export type UpsertWorkflowSearchAttributesEventAttributes =
 
 export type WorkflowExecutionStatus =
   temporal.api.enums.v1.WorkflowExecutionStatus;
+export type Severity = temporal.api.enums.v1.Severity;
 
 // api.workflow
 
diff --git a/static/logo.svg b/static/logo.svg
index b98101d6..ffa957bb 100644
--- a/static/logo.svg
+++ b/static/logo.svg
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg width="100px" height="100px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
  <!-- Generated by Pixelmator Pro 2.1.3 -->
- <path id="Rectangle" d="M-4.75 119.917 L106.583 119.917 106.583 -3.083 -4.75 -3.083 Z" fill="#000000" fill-opacity="1" stroke="none"/>
  <path id="Path" d="M65.816 34.224 C63.684 18.251 58.288 4.917 49.999 4.917 41.711 4.917 36.314 18.251 34.182 34.224 18.208 36.357 4.874 41.753 4.874 50.042 4.874 58.33 18.211 63.727 34.182 65.859 36.314 81.83 41.711 95.167 49.999 95.167 58.288 95.167 63.684 81.83 65.816 65.859 81.79 63.727 95.124 58.33 95.124 50.042 95.124 41.753 81.788 36.357 65.816 34.224 Z M33.661 61.206 C18.363 58.997 9.44 53.939 9.44 50.042 9.44 46.144 18.363 41.087 33.661 38.877 33.324 42.56 33.149 46.319 33.149 50.042 33.149 53.764 33.324 57.526 33.661 61.206 Z M49.999 9.483 C53.897 9.483 58.954 18.405 61.164 33.703 57.483 33.367 53.721 33.191 49.999 33.191 46.277 33.191 42.515 33.367 38.835 33.703 41.044 18.405 46.102 9.483 49.999 9.483 Z M66.338 61.206 C65.585 61.316 62.496 61.678 61.715 61.755 61.638 62.539 61.273 65.625 61.166 66.378 58.956 81.676 53.899 90.598 50.001 90.598 46.104 90.598 41.047 81.676 38.837 66.378 38.73 65.625 38.365 62.536 38.288 61.755 37.932 58.129 37.717 54.234 37.717 50.042 37.717 45.85 37.932 41.954 38.288 38.326 41.914 37.97 45.809 37.755 50.001 37.755 54.194 37.755 58.089 37.97 61.715 38.326 62.499 38.403 65.585 38.767 66.338 38.875 81.636 41.085 90.56 46.144 90.56 50.039 90.56 53.935 81.636 58.997 66.338 61.206 Z" fill="#ffffff" fill-opacity="1" stroke="none"/>
 </svg>
diff --git a/svelte.config.js b/svelte.config.js
index 7bae0126..96bcc287 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -46,9 +46,6 @@ const config = {
       fallback: 'index.html',
     }),
     vite: {
-      optimizeDeps: {
-        include: ['svelte-hero-icons'],
-      },
       resolve: {
         alias: {
           $types: path.resolve('./src/types'),


Test patch:
diff --git a/src/lib/models/activity.test.ts b/src/lib/models/activity.test.ts
new file mode 100644
index 00000000..32129333
--- /dev/null
+++ b/src/lib/models/activity.test.ts
@@ -0,0 +1,218 @@
+import { Activity, Activities } from './activity';
+
+const scheduledEvent = {
+  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',
+    },
+    input: null,
+  },
+};
+
+const anotherScheduledEvent = {
+  id: '99',
+  eventId: '99',
+  eventTime: '2021-12-27T15:23:21.806965200Z',
+  eventType: 'ActivityTaskScheduled',
+  version: '0',
+  taskId: '1049494',
+  activityTaskScheduledEventAttributes: {
+    activityId: '5',
+    activityType: {
+      name: 'FicticiousEvent',
+    },
+    namespace: '',
+    taskQueue: {
+      name: 'test-events',
+      kind: 'Normal',
+    },
+    input: null,
+  },
+};
+
+const startedEvent = {
+  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,
+  },
+};
+
+const completedEvent = {
+  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@',
+  },
+};
+
+const eventHistory = [
+  scheduledEvent,
+  startedEvent,
+  anotherScheduledEvent,
+  completedEvent,
+];
+
+describe(Activity, () => {
+  it('should grab the name of the ActivityTaskScheduledEvent', () => {
+    const activity = new Activity(scheduledEvent);
+
+    expect(activity.name).toBe('CompletedActivity');
+  });
+
+  it('should grab the id of the ActivityTaskScheduledEvent', () => {
+    const activity = new Activity(scheduledEvent);
+
+    expect(activity.id).toBe('5');
+  });
+
+  it('should store the ActivityTaskScheduled', () => {
+    const activity = new Activity(scheduledEvent);
+
+    expect(activity.get('ActivityTaskScheduled')).toBe(scheduledEvent);
+  });
+
+  it('should be able to add a started event', () => {
+    const activity = new Activity(scheduledEvent);
+    activity.set(completedEvent.eventType, completedEvent);
+
+    expect(activity.length).toBe(2);
+    expect(activity.get('ActivityTaskCompleted')).toBe(completedEvent);
+  });
+});
+
+describe(Activity, () => {
+  it('should create a new entry when given an ActivityTaskScheduled event', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+
+    const activity = activities.get(scheduledEvent.id);
+
+    expect(activity.get('ActivityTaskScheduled')).toBe(scheduledEvent);
+  });
+
+  it('should correctly set the name of the activity', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+
+    const activity = activities.get(scheduledEvent.id);
+
+    expect(activity.name).toBe('CompletedActivity');
+  });
+
+  it('should correctly set the id of the activity', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+
+    const activity = activities.get(scheduledEvent.id);
+
+    expect(activity.id).toBe('5');
+  });
+
+  it('should correctly set the name of the activity', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+
+    const activity = activities.get(scheduledEvent.id);
+
+    expect(activity.id).toBe('5');
+  });
+
+  it('should be able to store multiple activities', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+    activities.add(anotherScheduledEvent);
+
+    expect(activities.length).toBe(2);
+  });
+
+  it('should add a completed event to the correct activity', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+    activities.add(completedEvent);
+
+    const activity = activities.get(scheduledEvent.id);
+
+    expect(activity.length).toBe(2);
+    expect(activity.get('ActivityTaskCompleted')).toBe(completedEvent);
+  });
+
+  it('should be able to add multiple activities and their associated events', () => {
+    const activities = new Activities();
+
+    activities.add(scheduledEvent);
+    activities.add(startedEvent);
+    activities.add(completedEvent);
+    activities.add(anotherScheduledEvent);
+
+    const [first, second] = activities;
+
+    expect(activities.length).toBe(2);
+    expect(first.length).toBe(3);
+    expect(second.length).toBe(1);
+  });
+
+  describe('from event history', () => {
+    it('should create a collection with the correct number of activities', () => {
+      const activities = Activities.from(eventHistory);
+
+      const [first, second] = activities;
+
+      expect(activities.length).toBe(2);
+      expect(first.length).toBe(3);
+      expect(second.length).toBe(1);
+    });
+
+    it('should create a collection with the correct number of activities using the constructor', () => {
+      const activities = new Activities(eventHistory);
+
+      const [first, second] = activities;
+
+      expect(activities.length).toBe(2);
+      expect(first.length).toBe(3);
+      expect(second.length).toBe(1);
+    });
+
+    it('should instantiate correctly when given a single event', () => {
+      const activities = new Activities(scheduledEvent);
+
+      const [activity] = activities;
+
+      expect(activities.length).toBe(1);
+      expect(activity.length).toBe(1);
+    });
+  });
+});
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/get-event-classification.test.ts b/src/lib/utilities/get-event-classification.test.ts
index 3d8921ab..a9ad295c 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,
+  isPendingActivity,
+  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(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/get-workflow-execution-url.test.ts b/src/lib/utilities/get-workflow-execution-url.test.ts
deleted file mode 100644
index 0f3c918e..00000000
--- a/src/lib/utilities/get-workflow-execution-url.test.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { WorkflowExecution } from '../models/workflow-execution';
-import { getWorkflowExecutionUrl } from './get-workflow-execution-url';
-
-describe(getWorkflowExecutionUrl, () => {
-  it('should display a workflow URL with just the workflow information', () => {
-    const workflow = { id: 'id', runId: 'run' } as WorkflowExecution;
-
-    expect(getWorkflowExecutionUrl('default', workflow)).toBe(
-      '/namespaces/default/workflows/id/run',
-    );
-  });
-});
diff --git a/src/lib/utilities/get-workflow-filter-parameters.test.ts b/src/lib/utilities/get-workflow-filter-parameters.test.ts
new file mode 100644
index 00000000..1fcc8329
--- /dev/null
+++ b/src/lib/utilities/get-workflow-filter-parameters.test.ts
@@ -0,0 +1,75 @@
+import { getWorkflowFilterParameters } from './get-workflow-filter-parameters';
+
+describe(getWorkflowFilterParameters, () => {
+  afterEach(() => {
+    jest.useRealTimers();
+  });
+
+  it('should always have an earliest_time set', () => {
+    const result = getWorkflowFilterParameters();
+    expect(result['start_time_filter.earliest_time']).toBeDefined();
+  });
+
+  it('should always have a default earliest_time of 24 hours', () => {
+    jest.useFakeTimers().setSystemTime(new Date('2020-01-01').getTime());
+    const OneDayEarlier = '2019-12-31T00:00:00Z';
+
+    const result = getWorkflowFilterParameters();
+    expect(result['start_time_filter.earliest_time']).toBe(OneDayEarlier);
+  });
+
+  it('should adjust the start time based on a duration', () => {
+    jest.useFakeTimers().setSystemTime(new Date('2020-01-01').getTime());
+    const NinetyDaysEarlier = '2019-10-03T00:00:00Z';
+
+    const result = getWorkflowFilterParameters({ timeRange: { days: 90 } });
+    expect(result['start_time_filter.earliest_time']).toBe(NinetyDaysEarlier);
+  });
+
+  it('should pass through a workflow ID', () => {
+    const result = getWorkflowFilterParameters({ workflowId: 'string value' });
+    expect(result['execution_filter.workflow_id']).toBe('string value');
+  });
+
+  it('should pass through a workflow type', () => {
+    const result = getWorkflowFilterParameters({
+      workflowType: 'string value',
+    });
+    expect(result['type_filter.name']).toBe('string value');
+  });
+
+  it('should not include a workflow ID filter if none is provided', () => {
+    const result = getWorkflowFilterParameters();
+    expect(result['execution_filter.workflow_id']).toBeUndefined();
+  });
+
+  it('should not include a workflow ID filter if null is provided', () => {
+    const result = getWorkflowFilterParameters({ workflowId: null });
+    expect(result['execution_filter.workflow_id']).toBeUndefined();
+  });
+
+  it('should not include a workflow ID filter if an empty string is provided', () => {
+    const result = getWorkflowFilterParameters({ workflowId: '' });
+    expect(result['execution_filter.workflow_id']).toBeUndefined();
+  });
+
+  it('should not include a workflow type filter if none is provided', () => {
+    const result = getWorkflowFilterParameters();
+    expect(result['type_filter.name']).toBeUndefined();
+  });
+
+  it('should not include a workflow type filter if null is provided', () => {
+    const result = getWorkflowFilterParameters({ workflowType: null });
+    expect(result['type_filter.name']).toBeUndefined();
+  });
+
+  it('should not include a workflow type filter if an empty string is provided', () => {
+    const result = getWorkflowFilterParameters({ workflowType: '' });
+    expect(result['type_filter.name']).toBeUndefined();
+  });
+
+  it('should return a status filter if one is provided', () => {
+    const result = getWorkflowFilterParameters({ status: 'Running' });
+    expect(result['status_filter.status']).toBe('1');
+  });
+});
diff --git a/src/lib/utilities/get-workflow-status-filter-code.test.ts b/src/lib/utilities/get-workflow-status-filter-code.test.ts
new file mode 100644
index 00000000..72db878d
--- /dev/null
+++ b/src/lib/utilities/get-workflow-status-filter-code.test.ts
@@ -0,0 +1,31 @@
+import { getStatusFilterCode } from './get-workflow-status-filter-code';
+
+describe(getStatusFilterCode, () => {
+  it('should return "1" if the status is "Running"', () => {
+    expect(getStatusFilterCode('Running')).toBe('1');
+  });
+
+  it('should return "2" if the status is "Completed"', () => {
+    expect(getStatusFilterCode('Completed')).toBe('2');
+  });
+
+  it('should return "3" if the status is "Failed"', () => {
+    expect(getStatusFilterCode('Failed')).toBe('3');
+  });
+
+  it('should return "4" if the status is "Canceled"', () => {
+    expect(getStatusFilterCode('Canceled')).toBe('4');
+  });
+
+  it('should return "5" if the status is "Terminated"', () => {
+    expect(getStatusFilterCode('Terminated')).toBe('5');
+  });
+
+  it('should return "6" if the status is "ContinuedAsNew"', () => {
+    expect(getStatusFilterCode('ContinuedAsNew')).toBe('6');
+  });
+
+  it('should return "7" if the status is "TimedOut"', () => {
+    expect(getStatusFilterCode('TimedOut')).toBe('7');
+  });
+});
diff --git a/src/lib/utilities/route-for.test.ts b/src/lib/utilities/route-for.test.ts
new file mode 100644
index 00000000..69587b65
--- /dev/null
+++ b/src/lib/utilities/route-for.test.ts
@@ -0,0 +1,115 @@
+import { routeFor } from './route-for';
+
+describe(routeFor, () => {
+  it('should route to "workflows"', () => {
+    const path = routeFor('workflows', { namespace: 'default' });
+    expect(path).toBe('/namespaces/default/workflows');
+  });
+
+  it('should route to "workflow"', () => {
+    const path = routeFor('workflow', {
+      namespace: 'default',
+      workflowId: 'abc',
+      runId: 'def',
+    });
+    expect(path).toBe('/namespaces/default/workflows/abc/def');
+  });
+
+  it('should route to "workflow.events"', () => {
+    const path = routeFor('workflow.events', {
+      namespace: 'default',
+      workflowId: 'abc',
+      runId: 'def',
+    });
+    expect(path).toBe('/namespaces/default/workflows/abc/def/history');
+  });
+
+  it('should route to "workflow".events.full', () => {
+    const path = routeFor('workflow.events.full', {
+      namespace: 'default',
+      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',
+    );
+  });
+
+  it('should route to "workflow".events.json', () => {
+    const path = routeFor('workflow.events.json', {
+      namespace: 'default',
+      workflowId: 'abc',
+      runId: 'def',
+    });
+    expect(path).toBe('/namespaces/default/workflows/abc/def/history/json');
+  });
+
+  it('should route to "workflow".stack-trace', () => {
+    const path = routeFor('workflow.stack-trace', {
+      namespace: 'default',
+      workflowId: 'abc',
+      runId: 'def',
+    });
+    expect(path).toBe('/namespaces/default/workflows/abc/def/stack-trace');
+  });
+
+  it('should route to "workflow".query', () => {
+    const path = routeFor('workflow.query', {
+      namespace: 'default',
+      workflowId: 'abc',
+      runId: 'def',
+    });
+    expect(path).toBe('/namespaces/default/workflows/abc/def/query');
+  });
+
+  it('should route to "workers"', () => {
+    const path = routeFor('workers', {
+      namespace: 'default',
+      queue: 'rainbow-statuses',
+    });
+    expect(path).toBe('/namespaces/default/workers/rainbow-statuses');
+  });
+});
diff --git a/src/lib/utilities/update-query-parameters.test.ts b/src/lib/utilities/update-query-parameters.test.ts
new file mode 100644
index 00000000..085efafb
--- /dev/null
+++ b/src/lib/utilities/update-query-parameters.test.ts
@@ -0,0 +1,106 @@
+import { updateQueryParameters } from './update-query-parameters';
+
+const gotoOptions = { replaceState: true, keepfocus: true };
+
+describe(updateQueryParameters, () => {
+  it('should call the set method on the query when a value is provided', () => {
+    const parameter = 'parameter';
+    const value = 'value';
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = () => Promise.resolve(true);
+
+    const spy = jest.spyOn(query, 'set');
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(spy).toHaveBeenCalled();
+    expect(spy).toHaveBeenCalledWith(parameter, value);
+  });
+
+  it('should call the delete method on the query when no value is provided', () => {
+    const parameter = 'parameter';
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = () => Promise.resolve(true);
+
+    const spy = jest.spyOn(query, 'delete');
+
+    updateQueryParameters({ parameter, query, path, goto });
+
+    expect(spy).toHaveBeenCalled();
+    expect(spy).toHaveBeenCalledWith(parameter);
+  });
+
+  it('should call the delete method on the query when an empty string is provided', () => {
+    const parameter = 'parameter';
+    const value = '';
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = () => Promise.resolve(true);
+
+    const spy = jest.spyOn(query, 'delete');
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(spy).toHaveBeenCalled();
+    expect(spy).toHaveBeenCalledWith(parameter);
+  });
+
+  it('should call the delete method on the query when null is provided', () => {
+    const parameter = 'parameter';
+    const value = null;
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = () => Promise.resolve(true);
+
+    const spy = jest.spyOn(query, 'delete');
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(spy).toHaveBeenCalled();
+    expect(spy).toHaveBeenCalledWith(parameter);
+  });
+
+  it('should call `goto` with the correct path', () => {
+    const parameter = 'parameter';
+    const value = 'value';
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = jest.fn().mockReturnValue(Promise.resolve(null));
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(goto).toHaveBeenCalledWith(
+      '/some/path?parameter=value',
+      gotoOptions,
+    );
+  });
+
+  it('should call `goto` with the correct path when query paramters already exist', () => {
+    const parameter = 'parameter';
+    const value = 'value';
+    const query = new URLSearchParams({ otherParameter: 'otherValue' });
+    const path = '/some/path';
+    const goto = jest.fn().mockReturnValue(Promise.resolve(null));
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(goto).toHaveBeenCalledWith(
+      '/some/path?otherParameter=otherValue&parameter=value',
+      gotoOptions,
+    );
+  });
+
+  it('should call `goto` with without the "?" if the query params are empty', () => {
+    const parameter = 'parameter';
+    const value = null;
+    const query = new URLSearchParams();
+    const path = '/some/path';
+    const goto = jest.fn().mockReturnValue(Promise.resolve(null));
+
+    updateQueryParameters({ parameter, value, query, path, goto });
+
+    expect(goto).toHaveBeenCalledWith('/some/path', gotoOptions);
+  });
+});