Repository: temporalio/ui
Pull request #350 (2022-03-16 21:27:53)

Add pending activities and fix issue where compact event history would quickly show all events
- Brings back the compact view in the event view toggles.
- Removes unused components.
- Moves components only used for the event history to the route folder.

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

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

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/temporal-pizza/14662794-2882-4f91-881f-0c756b4129fa)


Patch:
diff --git a/src/lib/utilities/__mocks__/$app/env.ts b/__mocks__/$app/env.ts
similarity index 100%
rename from src/lib/utilities/__mocks__/$app/env.ts
rename to __mocks__/$app/env.ts
diff --git a/__mocks__/$app/navigation/goto.ts b/__mocks__/$app/navigation/goto.ts
new file mode 100644
index 00000000..1a00ce1b
--- /dev/null
+++ b/__mocks__/$app/navigation/goto.ts
@@ -0,0 +1,3 @@
+export const goto = jest.fn(() => '');
+
+export default goto;
diff --git a/jest.config.js b/jest.config.js
index 5c1a385e..746ce1b8 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -11,7 +11,7 @@ export default {
     '^\\$lib/(.*)$': '<rootDir>/src/lib/$1',
     '^\\$types/(.*)$': '<rootDir>/src/types/$1',
     '^\\$components/(.*)$': '<rootDir>/src/components/$1',
-    '^\\$app(.*)$': '<rootDir>/.svelte-kit/dev/runtime/app$1',
+    '^\\$app(.*)$': ['<rootDir>/.svelte-kit/runtime/app$1'],
   },
 
   coverageDirectory: './coverage',
diff --git a/src/app.postcss b/src/app.postcss
index 4b34bf98..9bbc8480 100644
--- a/src/app.postcss
+++ b/src/app.postcss
@@ -20,7 +20,3 @@ body {
   max-height: 25em;
   overflow-y: scroll;
 }
-
-select {
-  -webkit-appearance: none;
-}
diff --git a/src/global.d.ts b/src/global.d.ts
index 7d21ccd3..c613169c 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -52,6 +52,7 @@ type Settings = {
     enabled: boolean;
     options: string[];
   };
+  baseUrl: string;
   defaultNamespace: string;
   runtimeEnvironment: {
     isCloud: boolean;
diff --git a/src/lib/components/button.svelte b/src/lib/components/button.svelte
index 29034332..82ad35dd 100644
--- a/src/lib/components/button.svelte
+++ b/src/lib/components/button.svelte
@@ -7,21 +7,24 @@
   export let secondary: boolean = false;
   export let destroy: boolean = false;
   export let loading: boolean = false;
-  export let as: string = 'button';
+  export let login: boolean = false;
+  export let as: 'button' | 'anchor' = 'button';
   export let active: boolean = false;
   export let large: boolean = false;
   export let href: string = null;
   export let icon: IconDefinition = null;
+  export let classes: string = '';
 </script>
 
 {#if as === 'button'}
   <button
     on:click
-    class="flex items-center justify-center text-sm primary"
+    class={`flex items-center justify-center text-sm primary ${classes}`}
     class:selected={active}
     class:large
     class:secondary
     class:destroy
+    class:login
     {disabled}
   >
     {#if icon}
@@ -35,19 +38,22 @@
   <a
     {href}
     on:click
-    class="flex items-center justify-center text-sm primary"
+    class={`flex items-center justify-center text-sm primary w-fit ${classes}`}
     class:selected={active}
     class:large
     class:secondary
     class:destroy
     class:disabled
+    class:login
     {disabled}
-    >{#if loading}
-      <span class="animate-spin"> <Icon {icon} scale={1} /></span>
-    {:else}
-      <slot />
-    {/if}</a
   >
+    {#if icon}
+      <span class:animate-spin={loading} class="pr-2">
+        <Icon icon={loading ? faSpinner : icon} scale={1} />
+      </span>
+    {/if}
+    <slot />
+  </a>
 {/if}
 
 <style lang="postcss">
@@ -94,4 +100,8 @@
   .selected {
     @apply text-white bg-purple-600 border-purple-200;
   }
+
+  .login {
+    @apply bg-gray-900 mx-auto py-4;
+  }
 </style>
diff --git a/src/lib/components/empty-state.svelte b/src/lib/components/empty-state.svelte
index fc533e1a..b2766491 100644
--- a/src/lib/components/empty-state.svelte
+++ b/src/lib/components/empty-state.svelte
@@ -6,7 +6,7 @@
   export let content: string = '';
 </script>
 
-<div class="mt-12 flex flex-col justify-start items-center gap-2">
+<div class="my-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"
   >
diff --git a/src/lib/components/event-details.svelte b/src/lib/components/event-details.svelte
deleted file mode 100644
index 7482807e..00000000
--- a/src/lib/components/event-details.svelte
+++ /dev/null
@@ -1,36 +0,0 @@
-<script lang="ts">
-  import { format } from '$lib/utilities/format-camel-case';
-
-  import CodeBlock from './code-block.svelte';
-
-  const shouldDisplay = (key: string, value: unknown): boolean => {
-    if (value === null) return false;
-    if (value === undefined) return false;
-    if (value === '') return false;
-    if (value === '0s') return false;
-    if (key === 'type') return false;
-    return true;
-  };
-
-  export let attributes:
-    | EventAttribute
-    | PendingActivity
-    | Record<string, unknown>;
-</script>
-
-{#each Object.entries(attributes) as [key, value] (key)}
-  {#if shouldDisplay(key, value)}
-    <article
-      class="flex items-center content-start w-full border-b-2 last:border-b-0 border-gray-200 py-1"
-    >
-      <h4 class="w-96 flex-grow">{format(key)}</h4>
-      <div class="flex-grow w-full">
-        {#if typeof value === 'object'}
-          <CodeBlock content={value} />
-        {:else}
-          <p><span class="bg-gray-300 text-gray-700 px-2">{value}</span></p>
-        {/if}
-      </div>
-    </article>
-  {/if}
-{/each}
diff --git a/src/lib/components/event-label.svelte b/src/lib/components/event-label.svelte
deleted file mode 100644
index 9049365b..00000000
--- a/src/lib/components/event-label.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-<script lang="ts">
-  export let color: EventClassification | PendingActivity['state'] = null;
-</script>
-
-<span class="label {color} font-semibold">
-  <slot />
-</span>
-
-<style lang="postcss">
-  .label {
-    @apply px-2 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/lib/components/event-summary.svelte b/src/lib/components/event-summary.svelte
deleted file mode 100644
index 0e356d5e..00000000
--- a/src/lib/components/event-summary.svelte
+++ /dev/null
@@ -1,43 +0,0 @@
-<script lang="ts">
-  import { onMount } from 'svelte';
-  import Icon from 'svelte-fa';
-  import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons';
-
-  import { goto } from '$app/navigation';
-
-  export let expanded = false;
-  export let hash: string;
-
-  onMount(() => {
-    expanded = window.location.hash === hash;
-  });
-
-  const expand = () => {
-    expanded = !expanded;
-    if (expanded) {
-      goto(hash);
-    }
-  };
-</script>
-
-<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">
-      <slot />
-    </div>
-    <div class="flex justify-center items-center w-16">
-      <Icon
-        icon={expanded ? faAngleUp : faAngleDown}
-        scale={1.2}
-        class="block w-full h-full"
-      />
-    </div>
-  </div>
-  {#if expanded}<slot name="expanded" />{/if}
-</article>
-
-<style lang="postcss">
-  article:hover {
-    @apply cursor-pointer;
-  }
-</style>
diff --git a/src/lib/services/settings-service.ts b/src/lib/services/settings-service.ts
index a7f315fa..06f29edb 100644
--- a/src/lib/services/settings-service.ts
+++ b/src/lib/services/settings-service.ts
@@ -1,3 +1,4 @@
+import { browser } from '$app/env';
 import { getEnvironment } from '$lib/utilities/get-environment';
 import { requestFromAPI } from '$lib/utilities/request-from-api';
 import { routeForApi } from '$lib/utilities/route-for-api';
@@ -26,6 +27,8 @@ export const fetchSettings = async ({
       enabled: !!settings?.Auth?.Enabled,
       options: settings?.Auth?.Options,
     },
+    baseUrl:
+      import.meta?.env?.VITE_API ?? browser ? window.location.origin : '',
     defaultNamespace: settings?.DefaultNamespace || 'default', // API returns an empty string if default namespace is not configured
     runtimeEnvironment: {
       get isCloud() {
diff --git a/src/lib/utilities/get-login-url.ts b/src/lib/utilities/get-login-url.ts
new file mode 100644
index 00000000..6717540b
--- /dev/null
+++ b/src/lib/utilities/get-login-url.ts
@@ -0,0 +1,16 @@
+export const getLoginUrl = (
+  settings: Settings,
+  currentSearchParams: URLSearchParams,
+): string => {
+  const login = new URL('/auth/sso', settings.baseUrl);
+  const opts = settings.auth.options ?? [];
+
+  opts.forEach((option) => {
+    const searchParam = currentSearchParams.get(option);
+    if (searchParam) {
+      login.searchParams.set(option, searchParam);
+    }
+  });
+
+  return login.toString();
+};
diff --git a/src/lib/utilities/handle-error.ts b/src/lib/utilities/handle-error.ts
index c019cb62..23e4154d 100644
--- a/src/lib/utilities/handle-error.ts
+++ b/src/lib/utilities/handle-error.ts
@@ -1,9 +1,15 @@
+import { browser } from '$app/env';
+import { goto } from '$app/navigation';
 import { networkError } from '$lib/stores/error';
 import { notifications } from '../stores/notifications';
 import { isNetworkError } from './is-network-error';
 
 // This will eventually be expanded on.
 export const handleError = (error: unknown): void => {
+  if (isUnauthorized(error) && browser) {
+    goto('/login');
+    return;
+  }
   if (isForbidden(error)) {
     notifications.add('error', `${error.statusCode} ${error.statusText}`);
     return;
@@ -25,9 +31,17 @@ export const handleError = (error: unknown): void => {
   }
 };
 
+const isUnauthorized = (error: unknown): error is NetworkError => {
+  if (isNetworkError(error)) {
+    return error.statusCode === 401;
+  }
+
+  return false;
+};
+
 const isForbidden = (error: unknown): error is NetworkError => {
   if (isNetworkError(error)) {
-    return error.statusCode === 403 || error.statusCode === 401;
+    return error.statusCode === 403;
   }
 
   return false;
diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts
index 8ea43b2a..98d45a9f 100644
--- a/src/lib/utilities/route-for.ts
+++ b/src/lib/utilities/route-for.ts
@@ -3,8 +3,10 @@ type RoutePath =
   | 'workflow'
   | 'workflow.events'
   | 'workers'
-  | 'workflow.events.full'
-  | 'workflow.events.full.event'
+  | 'workflow.events.summary'
+  | 'workflow.events.summary.event'
+  | 'workflow.events.compact'
+  | 'workflow.events.compact.event'
   | 'workflow.events.json'
   | 'workflow.stack-trace'
   | 'workflow.query'
@@ -50,10 +52,13 @@ const routeForWorkflow = ({
 
 const routeForEventHistory = (
   parameters: WorkflowParameters,
-  view?: 'full' | 'json',
+  view?: 'summary' | 'compact' | 'json',
 ): string => {
   if (!view) return `${routeForWorkflow(parameters)}/history`;
-  if (view === 'full') return `${routeForWorkflow(parameters)}/history/summary`;
+  if (view === 'summary')
+    return `${routeForWorkflow(parameters)}/history/summary`;
+  if (view === 'compact')
+    return `${routeForWorkflow(parameters)}/history/compact`;
   if (view === 'json') return `${routeForWorkflow(parameters)}/history/json`;
 };
 
@@ -69,7 +74,8 @@ export function routeFor(
   path:
     | 'workflow'
     | 'workflow.events'
-    | 'workflow.events.full'
+    | 'workflow.events.summary'
+    | 'workflow.events.compact'
     | 'workflow.events.json'
     | 'workflow.stack-trace'
     | 'workflow.query'
@@ -77,7 +83,7 @@ export function routeFor(
   parameters: WorkflowParameters,
 ): string;
 export function routeFor(
-  path: 'workflow.events.full.event',
+  path: 'workflow.events.summary.event',
   parameters: EventParameter,
 ): string;
 export function routeFor(
@@ -99,8 +105,12 @@ export function routeFor(path: RoutePath, parameters: RouteParameters): string {
     route = routeForEventHistory(parameters);
   }
 
-  if (path === 'workflow.events.full') {
-    route = routeForEventHistory(parameters, 'full');
+  if (path === 'workflow.events.summary') {
+    route = routeForEventHistory(parameters, 'summary');
+  }
+
+  if (path === 'workflow.events.compact') {
+    route = routeForEventHistory(parameters, 'compact');
   }
 
   if (path === 'workflow.events.json') {
diff --git a/src/routes/_header.svelte b/src/routes/_header.svelte
index 08d15c5c..d46cfd46 100644
--- a/src/routes/_header.svelte
+++ b/src/routes/_header.svelte
@@ -10,20 +10,6 @@
   import { goto } from '$app/navigation';
 
   import NavigationLink from './_navigation-link.svelte';
-  import { page } from '$app/stores';
-
-  $: settings = $page.stuff.settings;
-
-  let loginUrl = '';
-
-  $: {
-    const query = (settings.auth.options ?? [])
-      .filter((option) => !!$page.url.searchParams.get(option))
-      .map((option) => `&${option}=${$page.url.searchParams.get(option)}`)
-      .join('');
-
-    loginUrl = import.meta.env.VITE_API + '/auth/sso?' + encodeURI(query);
-  }
 </script>
 
 <header
@@ -54,24 +40,19 @@
     >
       Report Bug/Give Feedback
     </a>
-    {#if settings.auth.enabled}
-      {#if $user?.email}
-        <button
-          class="header-button min-w-min"
-          on:click={() => goto(import.meta.env.VITE_API + '/auth/logout')}
-        >
-          Logout
-          <img
-            src={$user.picture}
-            alt="User Avatar"
-            class="rounded-full h-6 w-6 ml-2.5"
-          />
-        </button>
-      {:else}
-        <button class="header-button" on:click={() => goto(loginUrl)}>
-          Login
-        </button>
-      {/if}
+
+    {#if $user?.email}
+      <button
+        class="header-button min-w-min"
+        on:click={() => goto(import.meta.env.VITE_API + '/auth/logout')}
+      >
+        Logout
+        <img
+          src={$user.picture}
+          alt="User Avatar"
+          class="rounded-full h-6 w-6 ml-2.5"
+        />
+      </button>
     {/if}
   </div>
 </header>
diff --git a/src/routes/login/__layout.reset.svelte b/src/routes/login/__layout.reset.svelte
new file mode 100644
index 00000000..4fa864ce
--- /dev/null
+++ b/src/routes/login/__layout.reset.svelte
@@ -0,0 +1,1 @@
+<slot />
diff --git a/src/routes/login/index.svelte b/src/routes/login/index.svelte
new file mode 100644
index 00000000..fb429326
--- /dev/null
+++ b/src/routes/login/index.svelte
@@ -0,0 +1,54 @@
+<script context="module" lang="ts">
+  import { goto } from '$app/navigation';
+  import { page } from '$app/stores';
+
+  import Button from '$lib/components/button.svelte';
+  import { getLoginUrl } from '$lib/utilities/get-login-url';
+  import { faLock } from '@fortawesome/free-solid-svg-icons';
+
+  import { fetchSettings } from '$lib/services/settings-service';
+
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ url }) {
+    const settings: Settings = await fetchSettings({ url });
+
+    if (!settings.auth.enabled) {
+      return {
+        status: 404,
+      };
+    }
+
+    return {
+      props: { settings },
+      stuff: { settings },
+    };
+  };
+</script>
+
+<script lang="ts">
+  export let settings: Settings;
+</script>
+
+<header
+  class="grid grid-rows-1 grid-cols-12 px-10 items-center bg-gray-900 shadow-lg gap-6"
+>
+  <a href="/" class="flex my-4 max-h-8">
+    <img src="/logo.svg" alt="Temporal Logo" class="max-h-8" />
+  </a>
+</header>
+
+<section class="text-center my-[20vh]">
+  <h1 class="text-8xl font-semibold">Welcome back.</h1>
+  <p class="my-7">Lets get you signed in.</p>
+  <div class="mx-auto">
+    <Button
+      classes=""
+      login
+      icon={faLock}
+      on:click={() => {
+        goto(getLoginUrl(settings, $page.url.searchParams));
+      }}>Continue to SSO</Button
+    >
+  </div>
+</section>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_execution-information.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_execution-information.svelte
deleted file mode 100644
index ceb5cc16..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_execution-information.svelte
+++ /dev/null
@@ -1,11 +0,0 @@
-<script lang="ts">
-  export let title: string;
-  export let value: string | number | Long;
-</script>
-
-{#if value}
-  <div class="w-full mb-6">
-    <h3 class="text-lg w-full">{title}</h3>
-    <p class="text-sm text-gray-600">{value}</p>
-  </div>
-{/if}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
index 655f0273..9faea583 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
@@ -2,12 +2,19 @@
   import Icon from 'svelte-fa';
   import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
 
+  import { routeFor } from '$lib/utilities/route-for';
+
   import WorkflowStatus from '$lib/components/workflow-status.svelte';
   import TerminateWorkflow from '$lib/components/terminate-workflow.svelte';
-  import Tabs from './_tabs.svelte';
+  import Tab from '$lib/components/tab.svelte';
 
   export let namespace: string;
   export let workflow: WorkflowExecution;
+
+  const { id: workflowId, runId } = workflow;
+
+  const historyEvents = workflow?.historyEvents;
+  const routeParamters = { namespace, workflowId, runId };
 </script>
 
 <header class="flex flex-col gap-4">
@@ -35,5 +42,17 @@
       <span class="font-medium">{workflow.runId}</span>
     </p>
   </main>
-  <Tabs {workflow} />
+  <nav class="flex gap-6">
+    <Tab
+      label="History"
+      href={routeFor('workflow.events', routeParamters)}
+      amount={historyEvents}
+    />
+    <Tab label="Workers" href={routeFor('workers', routeParamters)} />
+    <Tab
+      label="Stack Trace"
+      href={routeFor('workflow.stack-trace', routeParamters)}
+    />
+    <Tab label="Query" href={routeFor('workflow.query', routeParamters)} />
+  </nav>
 </header>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
deleted file mode 100644
index e0ad5d59..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
+++ /dev/null
@@ -1,31 +0,0 @@
-<script lang="ts">
-  import { page } from '$app/stores';
-  import { routeFor } from '$lib/utilities/route-for';
-
-  import Tab from '$lib/components/tab.svelte';
-
-  export let workflow: WorkflowExecution;
-
-  const { namespace, workflow: workflowId, run: runId } = $page.params;
-  const historyEvents = workflow?.historyEvents;
-</script>
-
-<nav class="flex gap-6">
-  <Tab
-    label="History"
-    href={routeFor('workflow.events', { namespace, workflowId, runId })}
-    amount={historyEvents}
-  />
-  <Tab
-    label="Workers"
-    href={routeFor('workers', { namespace, workflowId, runId })}
-  />
-  <Tab
-    label="Stack Trace"
-    href={routeFor('workflow.stack-trace', { namespace, workflowId, runId })}
-  />
-  <Tab
-    label="Query"
-    href={routeFor('workflow.query', { namespace, workflowId, runId })}
-  />
-</nav>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_task-queue-information.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_task-queue-information.svelte
deleted file mode 100644
index f496be8c..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_task-queue-information.svelte
+++ /dev/null
@@ -1,12 +0,0 @@
-<script lang="ts">
-  export let title: string;
-  export let value: string | number;
-  export let href: string;
-</script>
-
-{#if value}
-  <div class="w-full mb-6">
-    <h3 class="text-lg w-full">{title}</h3>
-    <a {href} class="text-sm text-purple-500">{value}</a>
-  </div>
-{/if}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
index 4e805d3b..c1c25bd2 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
@@ -14,6 +14,7 @@
       props: {
         workflowParameters: { workflowId, runId, namespace },
         events,
+        eventGroups,
       },
       stuff: {
         events,
@@ -24,7 +25,11 @@
 </script>
 
 <script lang="ts">
-  import { faCode, faStream } from '@fortawesome/free-solid-svg-icons';
+  import {
+    faCode,
+    faLayerGroup,
+    faStream,
+  } from '@fortawesome/free-solid-svg-icons';
 
   import { routeFor } from '$lib/utilities/route-for';
   import { getWorkflowStartedAndCompletedEvents } from '$lib/utilities/get-started-and-completed-events';
@@ -33,11 +38,12 @@
   import ToggleButton from '$lib/components/toggle-button.svelte';
   import ToggleButtons from '$lib/components/toggle-buttons.svelte';
   import CodeBlock from '$lib/components/code-block.svelte';
+  import PendingActivties from './_pending-activties.svelte';
 
   export let workflowParameters: WorkflowParameters;
   export let events: HistoryEventWithId[];
 
-  let { input, result } = getWorkflowStartedAndCompletedEvents(events);
+  const { input, result } = getWorkflowStartedAndCompletedEvents(events);
 </script>
 
 <section class="flex flex-col gap-4">
@@ -45,6 +51,7 @@
     <CodeBlock heading="Input" content={input} framed />
     <CodeBlock heading="Result" content={result} framed />
   </div>
+  <PendingActivties />
   <section id="event-history">
     <nav class="flex gap-4 justify-between items-end pb-4">
       <h3 class="text-lg font-medium">Event History</h3>
@@ -52,7 +59,11 @@
         <ToggleButtons>
           <ToggleButton
             icon={faStream}
-            href={routeFor('workflow.events.full', workflowParameters)}
+            href={routeFor('workflow.events.summary', workflowParameters)}
+          />
+          <ToggleButton
+            icon={faLayerGroup}
+            href={routeFor('workflow.events.compact', workflowParameters)}
           />
           <ToggleButton
             icon={faCode}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-details.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-details.svelte
new file mode 100644
index 00000000..bfeae85c
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-details.svelte
@@ -0,0 +1,61 @@
+<script lang="ts">
+  import { format } from '$lib/utilities/format-camel-case';
+
+  import CodeBlock from '$lib/components/code-block.svelte';
+
+  export let event: HistoryEventWithId;
+  export let eventGroup: CompactEventGroup;
+
+  const shouldDisplay = (key: string, value: unknown): boolean => {
+    if (value === null) return false;
+    if (value === undefined) return false;
+    if (value === '') return false;
+    if (value === '0s') return false;
+    if (key === 'type') return false;
+    return true;
+  };
+</script>
+
+<section class="overflow-y-scroll max-h-full">
+  {#if eventGroup}
+    <nav class="flex flex-col mb-4">
+      <ul class="flex gap-4 w-full items-start">
+        {#each [...eventGroup.events] as [id, eventInGroup]}
+          <li>
+            <a
+              sveltekit:noscroll
+              href={id}
+              class:active={id === event.id}
+              class="border-b-2 border-blue-600"
+            >
+              {eventInGroup.eventType}
+            </a>
+          </li>
+        {/each}
+      </ul>
+    </nav>
+  {/if}
+
+  {#each Object.entries(event.attributes) as [key, value] (key)}
+    {#if shouldDisplay(key, value)}
+      <article
+        class="flex items-center content-start w-full border-b-2 last:border-b-0 border-gray-200 py-1"
+      >
+        <h4 class="w-96 flex-grow">{format(key)}</h4>
+        <div class="flex-grow w-full">
+          {#if typeof value === 'object'}
+            <CodeBlock content={value} />
+          {:else}
+            <p><span class="bg-gray-300 text-gray-700 px-2">{value}</span></p>
+          {/if}
+        </div>
+      </article>
+    {/if}
+  {/each}
+</section>
+
+<style lang="postcss">
+  .active {
+    @apply text-blue-700 border-b-2 border-blue-600;
+  }
+</style>
diff --git a/src/lib/components/event.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-list-item.svelte
similarity index 74%
rename from src/lib/components/event.svelte
rename to src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-list-item.svelte
index 31f6eb98..7e9ba196 100644
--- a/src/lib/components/event.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-list-item.svelte
@@ -6,8 +6,6 @@
   import { isEvent } from '$lib/models/event-history';
   import { isEventGroup } from '$lib/models/group-events';
 
-  import EventLabel from '$lib/components/event-label.svelte';
-
   export let event: HistoryEventWithId | CompactEventGroup;
 
   const isActive = (currentId: string): boolean => {
@@ -35,9 +33,9 @@
     <p class="w-5 text-center text-gray-500">{event.id}</p>
     <div class="w-full">
       <h2 class="mb-2">
-        <EventLabel color={event.classification}>
+        <span class="label {event.classification} font-semibold">
           {event.name}
-        </EventLabel>
+        </span>
       </h2>
       <p class="text-sm">
         <Icon icon={faCalendar} class="inline" />
@@ -63,4 +61,36 @@
   a:last-child {
     @apply border-b-0;
   }
+
+  .label {
+    @apply px-2 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]/history/_event-table.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-table.svelte
new file mode 100644
index 00000000..0d5bb9dd
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_event-table.svelte
@@ -0,0 +1,60 @@
+<script lang="ts">
+  import EmptyState from '$lib/components/empty-state.svelte';
+  import FilterSelect from '$lib/components/select/filter-select.svelte';
+  import Option from '$lib/components/select/option.svelte';
+  import Event from './_event-list-item.svelte';
+
+  export let events: HistoryEventWithId[] | CompactEventGroups;
+  export let category: EventTypeCategory;
+</script>
+
+<section class="flex flex-col border-2 border-gray-300 rounded-lg w-full  mb-6">
+  <div class="flex w-full">
+    <header class="table-header border-r-2 rounded-tl-lg w-1/3">
+      <h3>Summary</h3>
+      <div class="flex items-center gap-4">
+        <FilterSelect parameter="category" bind:value={category}>
+          <Option value={undefined}>All</Option>
+          <Option value="activity">Activity</Option>
+          <Option value="command">Command</Option>
+          <Option value="signal">Signal</Option>
+          <Option value="timer">Timer</Option>
+          <Option value="child-workflow">Child Workflow</Option>
+          <Option value="workflow">Workflow</Option>
+        </FilterSelect>
+      </div>
+    </header>
+    <header class="table-header rounded-tr-lg w-2/3">
+      <h3>Details</h3>
+    </header>
+  </div>
+  <div class="flex">
+    <div class="flex flex-col w-1/3 border-r-2 border-gray-300 rounded-bl-lg">
+      <div class="rounded-bl-lg overflow-y-scroll h-full">
+        {#each events as event (event.id)}
+          {#if !category || event.category === category}<Event {event} />{/if}
+        {:else}
+          <div class="p-2">
+            <EmptyState
+              title="No Events Match"
+              content="There are no events that match your filters. Adjust your filters to see your events."
+            />
+          </div>
+        {/each}
+      </div>
+    </div>
+    <div class="flex flex-col w-2/3">
+      <div
+        class="overflow-y-scroll overflow-x-hidden rounded-br-lg px-4 w-full py-4"
+      >
+        <slot />
+      </div>
+    </div>
+  </div>
+</section>
+
+<style lang="postcss">
+  .table-header {
+    @apply bg-gray-100 text-gray-800 font-semibold p-4 flex justify-between items-center border-b-2;
+  }
+</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_get-first-id.ts b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_get-first-id.ts
new file mode 100644
index 00000000..0f1c3e91
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_get-first-id.ts
@@ -0,0 +1,6 @@
+export const getFirstId = (
+  eventsOrEventGroups: HistoryEventWithId[] | CompactEventGroups,
+): string => {
+  const [first] = eventsOrEventGroups;
+  return first?.id;
+};
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_load-event-details.ts b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_load-event-details.ts
new file mode 100644
index 00000000..ca5deb01
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_load-event-details.ts
@@ -0,0 +1,22 @@
+import type { Load } from '@sveltejs/kit';
+
+import { getGroupForEvent } from '$lib/models/group-events';
+
+export const loadEventDetails: Load = async function ({ params, stuff }) {
+  const { eventId } = params;
+  const { events, eventGroups } = stuff;
+
+  const event: HistoryEventWithId = events.find(
+    (event: HistoryEventWithId) => event.id === eventId,
+  );
+
+  const eventGroup: CompactEventGroup = getGroupForEvent(event, eventGroups);
+
+  if (!event) {
+    return { status: 404 };
+  }
+
+  return {
+    props: { event, eventGroup },
+  };
+};
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_pending-activties.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_pending-activties.svelte
new file mode 100644
index 00000000..2d0a5def
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_pending-activties.svelte
@@ -0,0 +1,59 @@
+<script lang="ts">
+  import { onMount } from 'svelte';
+  import { page } from '$app/stores';
+
+  import { formatDate } from '$lib/utilities/format-date';
+
+  const { pendingActivities } = $page.stuff.workflow;
+  const pendingActivitiesHref = '../../pending-activities';
+
+  onMount(() => {
+    window.Prism.highlightAll();
+  });
+</script>
+
+{#if pendingActivities.length}
+  <section class="border-2 border-gray-300 rounded-lg p-4">
+    <h3 class="text-lg font-medium mb-4">Pending Activities</h3>
+    <table class="w-full table-auto space-x-4">
+      {#each pendingActivities as { id, ...pendingActivity }}
+        <tr class="border-b-2 border-gray-300 pb-4 last-of-type:border-b-0">
+          <th class="text-left font-normal text-gray-500">
+            <a href="{pendingActivitiesHref}#{id}">
+              {pendingActivity.activityId}
+            </a>
+          </th>
+          <td>
+            <div class="flex gap-2">
+              <h4>Activity Name</h4>
+              <p>
+                <span class="bg-gray-300 text-gray-700 px-2">
+                  {pendingActivity.activityType?.name}
+                </span>
+              </p>
+            </div>
+          </td>
+          <td>
+            <div class="flex gap-2">
+              <h3>Last Failure</h3>
+              <pre
+                style="padding: 0 1em; margin: 0"
+                class="rounded-lg"><code class="language-json">{pendingActivity.lastFailure}</code></pre>
+            </div>
+          </td>
+          <td>
+            <div class="flex gap-2">
+              <h4>Last Heartbeat Time</h4>
+              <p>{formatDate(pendingActivity.lastHeartbeatTime)}</p>
+            </div>
+          </td>
+        </tr>
+      {/each}
+    </table>
+    <div class="text-right">
+      <a href={pendingActivitiesHref} class="border-b-2 border-blue-600">
+        Show all
+      </a>
+    </div>
+  </section>
+{/if}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/[eventId].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/[eventId].svelte
new file mode 100644
index 00000000..8ff71c43
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/[eventId].svelte
@@ -0,0 +1,12 @@
+<script lang="ts" context="module">
+  export { loadEventDetails as load } from '../_load-event-details';
+</script>
+
+<script lang="ts">
+  import EventDetails from '../_event-details.svelte';
+
+  export let event: HistoryEventWithId;
+  export let eventGroup: CompactEventGroup;
+</script>
+
+<EventDetails {event} {eventGroup} />
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..b1af24ea
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
@@ -0,0 +1,31 @@
+<script context="module" lang="ts">
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ stuff, url }) {
+    const { eventGroups } = stuff;
+
+    const category = url.searchParams.get('category');
+
+    return {
+      props: {
+        eventGroups,
+        category,
+      },
+    };
+  };
+</script>
+
+<script lang="ts">
+  import EventTable from '../_event-table.svelte';
+
+  export let eventGroups: CompactEventGroups;
+  export let category: EventTypeCategory = null;
+
+  $: visibleEvents = eventGroups.filter(
+    (event) => !category || event.category === category,
+  );
+</script>
+
+<EventTable {category} events={visibleEvents}>
+  <slot />
+</EventTable>
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..4e0c464e
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/index.svelte
@@ -0,0 +1,11 @@
+<script context="module" lang="ts">
+  import type { Load } from '@sveltejs/kit';
+  import { getFirstId } from '../_get-first-id';
+
+  export const load: Load = async function ({ url, stuff }) {
+    return {
+      status: 302,
+      redirect: `${url.pathname}/${getFirstId(stuff.eventGroups)}`,
+    };
+  };
+</script>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte
index e02d8916..8ff71c43 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/[eventId].svelte
@@ -1,59 +1,12 @@
 <script lang="ts" context="module">
-  import type { Load } from '@sveltejs/kit';
-
-  export const load: Load = async function ({ params, stuff }) {
-    const { eventId } = params;
-    const { events, eventGroups } = stuff;
-
-    const event: HistoryEventWithId = events.find(
-      (event: HistoryEventWithId) => event.id === eventId,
-    );
-
-    const eventGroup: CompactEventGroup = getGroupForEvent(event, eventGroups);
-
-    if (!event) {
-      return { status: 404 };
-    }
-
-    return {
-      props: { attributes: event.attributes, eventGroup, eventId },
-    };
-  };
+  export { loadEventDetails as load } from '../_load-event-details';
 </script>
 
 <script lang="ts">
-  import EventDetails from '$lib/components/event-details.svelte';
-  import { getGroupForEvent } from '$lib/models/group-events';
+  import EventDetails from '../_event-details.svelte';
 
-  export let attributes: EventAttribute;
+  export let event: HistoryEventWithId;
   export let eventGroup: CompactEventGroup;
-  export let eventId: string;
 </script>
 
-<section class="overflow-y-scroll max-h-full">
-  {#if eventGroup}
-    <nav class="flex flex-col mb-4">
-      <ul class="flex gap-4 w-full items-start">
-        {#each [...eventGroup.events] as [id, event]}
-          <li>
-            <a
-              sveltekit:noscroll
-              href={id}
-              class:active={id === eventId}
-              class="border-b-2 border-blue-600"
-            >
-              {event.eventType}
-            </a>
-          </li>
-        {/each}
-      </ul>
-    </nav>
-  {/if}
-  <EventDetails {attributes} />
-</section>
-
-<style lang="postcss">
-  .active {
-    @apply text-blue-700 border-b-2 border-blue-600;
-  }
-</style>
+<EventDetails {event} {eventGroup} />
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte
index e8dd922e..28a9af50 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/__layout.svelte
@@ -2,94 +2,30 @@
   import type { Load } from '@sveltejs/kit';
 
   export const load: Load = async function ({ stuff, url }) {
-    const { events, eventGroups } = stuff;
+    const { events } = stuff;
 
     const category = url.searchParams.get('category');
-    const compact = !!url.searchParams.get('compact');
 
     return {
       props: {
         events,
-        eventGroups,
         category,
-        compact,
       },
     };
   };
 </script>
 
 <script lang="ts">
-  import EmptyState from '$lib/components/empty-state.svelte';
-  import Event from '$lib/components/event.svelte';
-  import FilterCheckbox from '$lib/components/filter-checkbox.svelte';
-  import FilterSelect from '$lib/components/select/filter-select.svelte';
-  import Option from '$lib/components/select/option.svelte';
+  import EventTable from '../_event-table.svelte';
 
   export let events: HistoryEventWithId[];
-  export let eventGroups: CompactEventGroups;
-
-  export let compact = false;
   export let category: EventTypeCategory = null;
 
-  $: eventsForView = compact ? eventGroups : events;
-  $: visibleEvents = [...eventsForView].filter(
-    (event: HistoryEventWithId | CompactEventGroup) =>
-      !category || event.category === category,
+  $: visibleEvents = events.filter(
+    (event) => !category || event.category === category,
   );
 </script>
 
-<section class="flex flex-col border-2 border-gray-300 rounded-lg w-full mb-6">
-  <div class="flex w-full">
-    <header class="table-header border-r-2 rounded-tl-lg w-1/3">
-      <h3>Summary</h3>
-      <div class="flex items-center gap-4">
-        <FilterSelect parameter="category" bind:value={category}>
-          <Option value={undefined}>All</Option>
-          <Option value="activity">Activity</Option>
-          <Option value="command">Command</Option>
-          <Option value="signal">Signal</Option>
-          <Option value="timer">Timer</Option>
-          <Option value="child-workflow">Child Workflow</Option>
-          <Option value="workflow">Workflow</Option>
-        </FilterSelect>
-        <FilterCheckbox
-          parameter="compact"
-          name="Compact"
-          bind:value={compact}
-        />
-      </div>
-    </header>
-    <header class="table-header rounded-tr-lg w-2/3">
-      <h3>Details</h3>
-    </header>
-  </div>
-  <div class="flex">
-    <div class="flex flex-col w-1/3 border-r-2 border-gray-300 rounded-bl-lg">
-      <div class="rounded-bl-lg overflow-y-scroll">
-        {#each visibleEvents as event (event.id)}
-          {#if !category || event.category === category}<Event {event} />{/if}
-        {:else}
-          <div class="p-2">
-            <EmptyState
-              title="No Events Match"
-              content="There are know events that match your filters. Adjust your filters to see your events."
-            />
-          </div>
-        {/each}
-      </div>
-    </div>
-    <div class="flex flex-col w-2/3">
-      <div
-        class="overflow-y-scroll overflow-x-hidden rounded-br-lg px-4 w-full py-4"
-      >
-        <slot />
-      </div>
-    </div>
-  </div>
-</section>
-
-<style lang="postcss">
-  .table-header {
-    @apply bg-gray-100 text-gray-800 font-semibold p-4 flex justify-between items-center border-b-2;
-  }
-</style>
+<EventTable {category} events={visibleEvents}>
+  <slot />
+</EventTable>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte
index 5f157973..ca86eee3 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/summary/index.svelte
@@ -1,12 +1,11 @@
 <script context="module" lang="ts">
   import type { Load } from '@sveltejs/kit';
+  import { getFirstId } from '../_get-first-id';
 
   export const load: Load = async function ({ url, stuff }) {
-    const [firstEvent] = stuff.events;
-
     return {
       status: 302,
-      redirect: `${url.pathname}/${firstEvent.id}`,
+      redirect: `${url.pathname}/${getFirstId(stuff.events)}`,
     };
   };
 </script>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/pending-activities.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/pending-activities.svelte
new file mode 100644
index 00000000..0ddaf050
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/pending-activities.svelte
@@ -0,0 +1,47 @@
+<script lang="ts">
+  import { page } from '$app/stores';
+  import CodeBlock from '$lib/components/code-block.svelte';
+
+  import { format } from '$lib/utilities/format-camel-case';
+
+  const { pendingActivities } = $page.stuff.workflow;
+</script>
+
+<section class="border-2 border-gray-300 rounded-lg w-full mb-6">
+  <header class="table-header rounded-t-lg">
+    <h3>Pending Activities</h3>
+  </header>
+  <div>
+    {#each pendingActivities as { id, activityId, ...details }}
+      <article class="flex gap-4">
+        <div {id} class="p-4">
+          <a href={id} class="block py-1">{activityId}</a>
+        </div>
+        <div class="p-4 w-full">
+          {#each Object.entries(details) as [key, value] (key)}
+            <article
+              class="flex items-start content-start w-full border-b-2 last:border-b-0 border-gray-200 py-1"
+            >
+              <h4 class="w-full flex-grow">{format(key)}</h4>
+              <div class="flex-grow w-full">
+                {#if typeof value === 'object'}
+                  <CodeBlock content={value} />
+                {:else}
+                  <p>
+                    <span class="bg-gray-300 text-gray-700 px-2">{value}</span>
+                  </p>
+                {/if}
+              </div>
+            </article>
+          {/each}
+        </div>
+      </article>
+    {/each}
+  </div>
+</section>
+
+<style lang="postcss">
+  .table-header {
+    @apply bg-gray-100 text-gray-800 font-semibold p-4 flex justify-between items-center border-b-2;
+  }
+</style>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/query.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/query.svelte
index a3cc50c0..ba93fbdc 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/query.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/query.svelte
@@ -22,7 +22,7 @@
   import Select from '$lib/components/select/select.svelte';
 
   export let namespace: string;
-  export let workflow;
+  export let workflow: { id: string; runId: string };
 
   let queryType: string;
 
diff --git a/src/routes/signin/__layout.reset.svelte b/src/routes/signin/__layout.reset.svelte
new file mode 100644
index 00000000..4fa864ce
--- /dev/null
+++ b/src/routes/signin/__layout.reset.svelte
@@ -0,0 +1,1 @@
+<slot />
diff --git a/src/routes/signin/index.svelte b/src/routes/signin/index.svelte
new file mode 100644
index 00000000..fb429326
--- /dev/null
+++ b/src/routes/signin/index.svelte
@@ -0,0 +1,54 @@
+<script context="module" lang="ts">
+  import { goto } from '$app/navigation';
+  import { page } from '$app/stores';
+
+  import Button from '$lib/components/button.svelte';
+  import { getLoginUrl } from '$lib/utilities/get-login-url';
+  import { faLock } from '@fortawesome/free-solid-svg-icons';
+
+  import { fetchSettings } from '$lib/services/settings-service';
+
+  import type { Load } from '@sveltejs/kit';
+
+  export const load: Load = async function ({ url }) {
+    const settings: Settings = await fetchSettings({ url });
+
+    if (!settings.auth.enabled) {
+      return {
+        status: 404,
+      };
+    }
+
+    return {
+      props: { settings },
+      stuff: { settings },
+    };
+  };
+</script>
+
+<script lang="ts">
+  export let settings: Settings;
+</script>
+
+<header
+  class="grid grid-rows-1 grid-cols-12 px-10 items-center bg-gray-900 shadow-lg gap-6"
+>
+  <a href="/" class="flex my-4 max-h-8">
+    <img src="/logo.svg" alt="Temporal Logo" class="max-h-8" />
+  </a>
+</header>
+
+<section class="text-center my-[20vh]">
+  <h1 class="text-8xl font-semibold">Welcome back.</h1>
+  <p class="my-7">Lets get you signed in.</p>
+  <div class="mx-auto">
+    <Button
+      classes=""
+      login
+      icon={faLock}
+      on:click={() => {
+        goto(getLoginUrl(settings, $page.url.searchParams));
+      }}>Continue to SSO</Button
+    >
+  </div>
+</section>


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 0821b654..c3659c50 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -13,5 +13,7 @@ jobs:
       - uses: actions/checkout@v2
       - name: Install modules
         run: npm ci
+      - name: Populate .svelte-dir with files for jest
+        run: npm run build:local
       - name: Run tests
         run: npm test
diff --git a/src/lib/utilities/get-login-url.test.ts b/src/lib/utilities/get-login-url.test.ts
new file mode 100644
index 00000000..5c90f582
--- /dev/null
+++ b/src/lib/utilities/get-login-url.test.ts
@@ -0,0 +1,70 @@
+import { getLoginUrl } from './get-login-url';
+
+describe('Get login url ', () => {
+  it('Options added through settings should be passed in the url', () => {
+    const settings = {
+      auth: {
+        options: ['one'],
+      },
+      baseUrl: 'https://localhost/',
+    };
+
+    const params = new URLSearchParams();
+    params.set('one', '1');
+    params.set('two', '2');
+
+    const login = getLoginUrl(settings, params);
+
+    const loginUrl = new URL(login);
+
+    expect(loginUrl.searchParams.get('one')).toBe('1');
+    expect(loginUrl.searchParams.get('two')).toBeNull();
+  });
+  it("We should not add the options from the search param if they don't exist in the current url params", () => {
+    const settings = {
+      auth: {
+        options: ['one'],
+      },
+      baseUrl: 'https://localhost/',
+    };
+
+    const params = new URLSearchParams();
+
+    const login = getLoginUrl(settings, params);
+
+    const loginUrl = new URL(login);
+
+    expect(loginUrl.searchParams.get('one')).toBeNull();
+    expect(login).toEqual('https://localhost/auth/sso');
+  });
+  it('Should render a login url', () => {
+    const settings = { auth: {}, baseUrl: 'https://localhost' };
+
+    const params = new URLSearchParams();
+
+    const login = getLoginUrl(settings, params);
+
+    expect(login).toEqual('https://localhost/auth/sso');
+  });
+  it('test of the signin flow', () => {
+    const settings = {
+      auth: {
+        options: ['organization_name', 'invitation'],
+      },
+      baseUrl: 'https://localhost/',
+    };
+
+    const params = new URLSearchParams(
+      'invitation=Wwv6g2cKkfjyqoLxnCPUCfiKcjHKpK%5B%E2%80%A6%5Dn9ipxcao0jKYH0I3&organization_name=temporal-cloud',
+    );
+
+    const login = getLoginUrl(settings, params);
+
+    const loginUrl = new URL(login);
+
+    expect(loginUrl.searchParams.get('one')).toBeNull();
+    expect(login).toEqual(
+      'https://localhost/auth/sso?organization_name=temporal-cloud&invitation=Wwv6g2cKkfjyqoLxnCPUCfiKcjHKpK%5B%E2%80%A6%5Dn9ipxcao0jKYH0I3',
+    );
+  });
+});
diff --git a/src/lib/utilities/paginated.test.ts b/src/lib/utilities/paginated.test.ts
index 2c1f16b0..1f8c2ba1 100644
--- a/src/lib/utilities/paginated.test.ts
+++ b/src/lib/utilities/paginated.test.ts
@@ -1,3 +1,4 @@
+jest.mock('$app/navigation', () => jest.fn({ goto: () => false } as any));
 import { paginated } from './paginated';
 
 const fetchMock = async (token: string | Uint8Array = null) => {
diff --git a/src/lib/utilities/route-for.test.ts b/src/lib/utilities/route-for.test.ts
index 64ae5b51..65c9a36c 100644
--- a/src/lib/utilities/route-for.test.ts
+++ b/src/lib/utilities/route-for.test.ts
@@ -25,7 +25,7 @@ describe(routeFor, () => {
   });
 
   it('should route to "workflow".events.full', () => {
-    const path = routeFor('workflow.events.full', {
+    const path = routeFor('workflow.events.summary', {
       namespace: 'default',
       workflowId: 'abc',
       runId: 'def',