Repository: temporalio/ui
Pull request #201 (2022-01-07 04:53:29)

Auto-refresh fetching events
- Auto refreshes fetching tabs and routes
- Fixes minor issues with tabs
- Removes `stuff` in favor of `getContext` and `setContext`.


Patch:
diff --git a/src/events.d.ts b/src/events.d.ts
index 4fbb984a..a9fde83b 100644
--- a/src/events.d.ts
+++ b/src/events.d.ts
@@ -378,3 +378,5 @@ type EventTypeCategory =
 type ActivityEvent = HistoryEventWithId & {
   eventType: ActivityType;
 };
+
+type EventualHistoryEvents = PromiseLike<HistoryEventWithId[]>;
diff --git a/src/lib/components/event.svelte b/src/lib/components/event.svelte
index b33029b0..5bf304dd 100644
--- a/src/lib/components/event.svelte
+++ b/src/lib/components/event.svelte
@@ -1,9 +1,9 @@
 <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 { page } from '$app/stores';
+
+  import type { Activity } from '$lib/models/activity';
 
   import { formatEvent } from '$lib/utilities/get-event-classification';
   import { formatDate } from '$lib/utilities/format-date';
@@ -12,12 +12,11 @@
 
   export let event: HistoryEventWithId | PendingActivity | Activity;
 
-  let { id, routeFor, pending, timeStamp, name, tag, classification } =
+  let { 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);
+  let href = routeFor({ namespace, workflowId, runId });
 </script>
 
 <a
diff --git a/src/lib/components/tab.svelte b/src/lib/components/tab.svelte
index ff7660ed..b902e312 100644
--- a/src/lib/components/tab.svelte
+++ b/src/lib/components/tab.svelte
@@ -6,7 +6,7 @@
   export let label: string;
   export let amount: number | Long.Long = null;
 
-  $: active = pathMatches(href, $page.path);
+  $: active = $page.path.includes(href);
 </script>
 
 <a class="block" class:active {href}>
diff --git a/src/lib/models/activity.ts b/src/lib/models/activity.ts
index 90832557..15bb8d31 100644
--- a/src/lib/models/activity.ts
+++ b/src/lib/models/activity.ts
@@ -35,7 +35,7 @@ export class Activity {
     this.set(event.eventType, event);
   }
 
-  set(type: ActivityType, event: HistoryEventWithId) {
+  set(type: ActivityType, event: HistoryEventWithId): void {
     this._events.set(type, event);
   }
 
@@ -47,12 +47,12 @@ export class Activity {
     return [...this];
   }
 
-  get events(): typeof this._events {
-    return this._events;
+  get events(): HistoryEventWithId[] {
+    return [...this];
   }
 
   get length(): number {
-    return this.events.size;
+    return this._events.size;
   }
 
   get last(): HistoryEventWithId {
@@ -65,7 +65,7 @@ export class Activity {
     return last;
   }
 
-  [Symbol.iterator]() {
+  [Symbol.iterator](): IterableIterator<HistoryEventWithId> {
     return this._events.values();
   }
 }
@@ -73,6 +73,12 @@ export class Activity {
 export class Activities {
   private _activities: Map<string, Activity> = new Map();
 
+  static async fromPromise(
+    events: PromiseLike<HistoryEventWithId[]>,
+  ): Promise<Activities> {
+    return Activities.from(await events);
+  }
+
   static from = (
     events: HistoryEventWithId[],
     activities = new Activities(),
@@ -91,45 +97,45 @@ export class Activities {
     if (event) this.add(event);
   }
 
-  get(id: string | number | Long) {
+  get(id: string | number | Long): Activity {
     return this._activities.get(String(id));
   }
 
-  add(event: ActivityEvent) {
+  add(event: ActivityEvent): void {
     if (isActivityScheduledEvent(event)) {
       const id = String(event.id);
-      return this._activities.set(id, new Activity(event));
+      this._activities.set(id, new Activity(event));
     }
 
     if (event.eventType === 'ActivityTaskStarted') {
       const { scheduledEventId } = event.activityTaskStartedEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
 
     if (event.eventType === 'ActivityTaskCanceled') {
       const { scheduledEventId } = event.activityTaskCanceledEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
 
     if (event.eventType === 'ActivityTaskCancelRequested') {
       const { scheduledEventId } =
         event.activityTaskCancelRequestedEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
 
     if (event.eventType === 'ActivityTaskFailed') {
       const { scheduledEventId } = event.activityTaskFailedEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
 
     if (event.eventType === 'ActivityTaskTimedOut') {
       const { scheduledEventId } = event.activityTaskTimedOutEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
 
     if (event.eventType === 'ActivityTaskCompleted') {
       const { scheduledEventId } = event.activityTaskCompletedEventAttributes;
-      return this.get(scheduledEventId).set(event.eventType, event);
+      this.get(scheduledEventId).set(event.eventType, event);
     }
   }
 
@@ -141,7 +147,7 @@ export class Activities {
     return [...this].slice(...args);
   }
 
-  [Symbol.iterator]() {
+  [Symbol.iterator](): IterableIterator<Activity> {
     return this._activities.values();
   }
 }
diff --git a/src/lib/services/events-service.ts b/src/lib/services/events-service.ts
index c75935ad..09c4da62 100644
--- a/src/lib/services/events-service.ts
+++ b/src/lib/services/events-service.ts
@@ -1,8 +1,14 @@
+import { get } from 'svelte/store';
+
 import type { GetWorkflowExecutionHistoryResponse } from '$types';
 
+import { convertEventPayloadFromDataConverter } from './data-converter';
+import { dataConverterPort } from '$lib/stores/data-converter-config';
+
 import { paginated } from '$lib/utilities/paginated';
 import { requestFromAPI } from '$lib/utilities/request-from-api';
 import { routeForApi } from '$lib/utilities/route-for-api';
+import { toEventHistory } from '$lib/models/event-history';
 
 type FetchEvents = NamespaceScopedRequest &
   PaginationCallbacks<GetWorkflowExecutionHistoryResponse> & {
@@ -22,14 +28,16 @@ export const fetchEvents = async (
     onComplete,
   }: FetchEvents,
   request = fetch,
-): Promise<GetWorkflowExecutionHistoryResponse> => {
+): Promise<HistoryEventWithId[]> => {
   let params = {};
 
+  const port = get(dataConverterPort);
+
   if (rawPayloads) {
-    params = { params: { rawPayloads: JSON.stringify(true) } };
+    params = { params: { rawPayloads: JSON.stringify(!!port) } };
   }
 
-  const events: GetWorkflowExecutionHistoryResponse = await paginated(
+  const events = await paginated(
     async (token: string) => {
       return requestFromAPI<GetWorkflowExecutionHistoryResponse>(
         routeForApi('events', { namespace, executionId, runId }),
@@ -41,7 +49,18 @@ export const fetchEvents = async (
       );
     },
     { onStart, onUpdate, onComplete },
-  );
+  )
+    .then(toEventHistory)
+    .then(async (events) => {
+      if (port !== null) {
+        try {
+          await convertEventPayloadFromDataConverter(events, port);
+        } catch (error) {
+          console.error(error);
+        }
+      }
+      return Promise.resolve(events);
+    });
 
   return events;
 };
diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts
index 3403e8a3..6e94af3d 100644
--- a/src/lib/services/workflow-service.ts
+++ b/src/lib/services/workflow-service.ts
@@ -1,21 +1,15 @@
-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 { getWorkflowFilterParameters } from '$lib/utilities/get-workflow-filter-parameters';
-import { dataConverterPort } from '$lib/stores/data-converter-config';
 
 import {
   toWorkflowExecution,
   toWorkflowExecutions,
 } from '$lib/models/workflow-execution';
-import { toEventHistory } from '$lib/models/event-history';
 
-import { fetchEvents } from './events-service';
-import { convertEventPayloadFromDataConverter } from './data-converter';
 import { routeForApi } from '$lib/utilities/route-for-api';
+import { getWorkflowFilterParameters } from '$lib/utilities/get-workflow-filter-parameters';
 
 export type GetWorkflowExecutionRequest = NamespaceScopedRequest & {
   executionId: string;
@@ -109,35 +103,3 @@ export async function fetchWorkflow(
     toWorkflowExecution,
   );
 }
-
-export async function fetchWorkflowWithEventHistory(
-  parameters: GetWorkflowExecutionRequest,
-  request = fetch,
-): Promise<{ workflow: WorkflowExecution; events: HistoryEventWithId[] }> {
-  const port = get(dataConverterPort);
-
-  const [workflow, events] = await Promise.all([
-    fetchWorkflow(parameters, request),
-    fetchEvents({ ...parameters, rawPayloads: Boolean(port) }, request)
-      .then(toEventHistory)
-      .then(async (events) => {
-        if (port !== null) {
-          try {
-            // This is not my favorite code, but it mutates the events object inside the function call.
-            // we should definitely refactor this but this was essentially pulled from the original web project
-            // we can write this better using some Svelte primitives
-            await convertEventPayloadFromDataConverter(events, port);
-          } catch {
-            // This code is a bit side effecty, but the convert function handles it's error
-            // by just ignoring it and setting the error state for dataConverter then sending back the
-            // original payload.
-          }
-        }
-
-        // No matter what we want to return events. Even if they aren't data-converted
-        return Promise.resolve(events);
-      }),
-  ]);
-
-  return { workflow, events };
-}
diff --git a/src/lib/stores/refreshable.ts b/src/lib/stores/refreshable.ts
index 0323cc8a..3de7f665 100644
--- a/src/lib/stores/refreshable.ts
+++ b/src/lib/stores/refreshable.ts
@@ -9,10 +9,10 @@ type Timeout = Parameters<SetIntervalType>[1];
 
 export const refreshable = <T>(
   callback: () => PromiseLike<T>,
-  initialData: PromiseLike<T> = callback(),
+  initialData: PromiseLike<T> | T = callback(),
   timeout: Timeout = 10000,
 ): Writable<PromiseLike<T>> => {
-  const store = writable<PromiseLike<T>>(initialData);
+  const store = writable<PromiseLike<T>>(Promise.resolve(initialData));
   let interval: Timer;
 
   onMount(() => {
diff --git a/src/lib/utilities/get-event-classification.ts b/src/lib/utilities/get-event-classification.ts
index 8d786ff5..6aaf9a17 100644
--- a/src/lib/utilities/get-event-classification.ts
+++ b/src/lib/utilities/get-event-classification.ts
@@ -1,7 +1,8 @@
 import { Activity } from '../models/activity';
 import { format } from './format-camel-case';
 import { routeFor } from './route-for';
-import type { EventParameter } from './route-for';
+
+import type { WorkflowParameters } from './route-for';
 
 export type EventClassification = typeof eventClassifications[number];
 type EventOrActivity = HistoryEventWithId | PendingActivity | Activity;
@@ -15,7 +16,7 @@ type EventSummary = {
   pending: boolean;
   activity: boolean;
   type: 'event' | 'pending-activity' | 'activity';
-  routeFor: (parameters: EventParameter) => string;
+  routeFor: (parameters: WorkflowParameters) => string;
 };
 
 export const eventClassifications = [
@@ -104,14 +105,29 @@ const getType = (
 };
 
 export const getHref = (
-  event: EventOrActivity,
-  parameters: EventParameter,
+  event: EventOrActivity | PendingActivity,
+  parameters: WorkflowParameters,
 ): 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);
+  if (isEvent(event)) {
+    return routeFor('workflow.events.full.event', {
+      ...parameters,
+      eventId: String(event.eventId),
+    });
+  }
+
+  if (isPendingActivity(event)) {
+    return routeFor('workflow.events.full.pending', {
+      ...parameters,
+      eventId: String(event.activityId),
+    });
+  }
+
+  if (isActivity(event)) {
+    return routeFor('workflow.events.compact.activity', {
+      ...parameters,
+      eventId: String(event.id),
+    });
+  }
 };
 
 export const formatEvent = (
@@ -126,6 +142,6 @@ export const formatEvent = (
     type: getType(event),
     pending: isPendingActivity(event),
     activity: isActivity(event),
-    routeFor: (parameters: EventParameter) => getHref(event, parameters),
+    routeFor: (parameters: WorkflowParameters) => getHref(event, parameters),
   };
 };
diff --git a/src/lib/utilities/get-visible-events.ts b/src/lib/utilities/get-visible-events.ts
new file mode 100644
index 00000000..2b70ed8d
--- /dev/null
+++ b/src/lib/utilities/get-visible-events.ts
@@ -0,0 +1,15 @@
+import type { WorkflowExecution } from '$lib/models/workflow-execution';
+import { eventTypeInCategory } from './get-event-categorization';
+
+export const getVisibleEvents = async (
+  eventsRequest: EventualHistoryEvents,
+  workflowRequest: PromiseLike<WorkflowExecution>,
+  category: EventTypeCategory,
+): Promise<(HistoryEventWithId | PendingActivity)[]> => {
+  const events = await eventsRequest;
+  const workflow = await workflowRequest;
+  const pendingActivities = workflow.pendingActivities;
+  const visibleEvents = events.filter(eventTypeInCategory(category));
+
+  return [...pendingActivities, ...visibleEvents];
+};
diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts
index 35c0e2bc..fe8bce9b 100644
--- a/src/lib/utilities/route-for.ts
+++ b/src/lib/utilities/route-for.ts
@@ -69,9 +69,9 @@ const routeForEventHistoryItem = (
   parameters: WorkflowParameters,
   view: EventHistoryView,
   eventType: EventView,
-  eventId: string,
+  id: string,
 ): string => {
-  return `${routeForEventHistory(parameters, view)}/${eventType}-${eventId}`;
+  return `${routeForEventHistory(parameters, view)}/${eventType}-${id}`;
 };
 
 const routeForActivity = (
@@ -87,8 +87,8 @@ const routeForActivity = (
   )}/${eventType}-${eventId}/events/${activityId}`;
 };
 
-const routeForWorkers = ({ queue, ...parameters }: TaskQueueParameter) => {
-  return `${routeForNamespace(parameters)}/workers/${queue}`;
+const routeForWorkers = (parameters: WorkflowParameters) => {
+  return `${routeForWorkflow(parameters)}/workers`;
 };
 
 export function routeFor(
@@ -103,7 +103,8 @@ export function routeFor(
     | 'workflow.events.compact'
     | 'workflow.events.json'
     | 'workflow.stack-trace'
-    | 'workflow.query',
+    | 'workflow.query'
+    | 'workers',
   parameters: WorkflowParameters,
 ): string;
 export function routeFor(
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
index abfda966..8d30f1ed 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
@@ -1,33 +1,36 @@
 <script context="module" lang="ts">
   import type { LoadInput } from '@sveltejs/kit';
-  import { fetchWorkflowWithEventHistory } from '$lib/services/workflow-service';
+  import { fetchWorkflow } from '$lib/services/workflow-service';
 
   export async function load({ page }: LoadInput) {
     const { workflow: executionId, run: runId, namespace } = page.params;
-    const { workflow, events } = await fetchWorkflowWithEventHistory({
+
+    const parameters = {
       namespace,
       executionId,
       runId,
-    });
+    };
 
     return {
-      props: { workflow, namespace, executionId, runId },
-      stuff: { workflow, events },
+      props: { parameters, namespace },
     };
   }
 </script>
 
 <script lang="ts">
+  import { setContext } from 'svelte';
+  import { refreshable } from '$lib/stores/refreshable';
+
   import Header from './_header.svelte';
-  import type { WorkflowExecution } from '$lib/models/workflow-execution';
 
-  export let workflow: WorkflowExecution;
+  export let parameters: Parameters<typeof fetchWorkflow>[0];
   export let namespace: string;
-  export let executionId: string;
-  export let runId: string;
+
+  let workflow = refreshable(() => fetchWorkflow(parameters));
+  $: setContext('workflow', $workflow);
 </script>
 
 <main class="flex flex-col gap-4 h-full">
-  <Header {workflow} {namespace} />
+  <Header {namespace} />
   <slot />
 </main>
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
index 08999b0d..1f887cda 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_header.svelte
@@ -1,40 +1,44 @@
 <script lang="ts">
+  import { getContext } from 'svelte';
   import Icon from 'svelte-fa';
   import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
 
   import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
   import WorkflowStatus from '$lib/components/workflow-status.svelte';
   import TerminateWorkflow from '$lib/components/terminate-workflow.svelte';
   import Tabs from './_tabs.svelte';
 
-  export let workflow: WorkflowExecution;
+  export let workflow = getContext<PromiseLike<WorkflowExecution>>('workflow');
   export let namespace: string;
 </script>
 
-<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}
-        <WorkflowStatus status={workflow?.status} />
-      </h1>
-      <TerminateWorkflow {workflow} {namespace} />
-    </div>
-    <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>
+{#await workflow then workflow}
+  <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}
+          <WorkflowStatus status={workflow?.status} />
+        </h1>
+        <TerminateWorkflow {workflow} {namespace} />
+      </div>
+      <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>
+{/await}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
index 5cc3cf1a..d04ecfb3 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_tabs.svelte
@@ -20,10 +20,7 @@
   />
   <Tab
     label="Workers"
-    href={routeFor('workers', {
-      namespace,
-      queue: workflow.taskQueue,
-    })}
+    href={routeFor('workers', { namespace, workflowId, runId })}
   />
   <Tab
     label="Stack Trace"
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 66f457b4..203d0cf5 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/__layout.svelte
@@ -4,16 +4,21 @@
 
   export async function load({ page }: LoadInput) {
     const { workflow: workflowId, run: runId, namespace } = page.params;
+    const parameters = { namespace, executionId: workflowId, runId };
+
+    const events = await fetchEvents(parameters);
 
     return {
       props: {
         workflow: { workflowId, runId, namespace },
+        parameters,
       },
     };
   }
 </script>
 
 <script lang="ts">
+  import { setContext } from 'svelte';
   import {
     faCode,
     faDownload,
@@ -21,11 +26,18 @@
     faStream,
   } from '@fortawesome/free-solid-svg-icons';
 
+  import { refreshable } from '$lib/stores/refreshable';
+
   import ToggleButton from '$lib/components/toggle-button.svelte';
   import ToggleButtons from '$lib/components/toggle-buttons.svelte';
   import { routeFor } from '$lib/utilities/route-for';
+  import { fetchEvents } from '$lib/services/events-service';
 
   export let workflow: WorkflowParameters;
+  export let parameters: Parameters<typeof fetchEvents>[0];
+
+  let events = refreshable(() => fetchEvents(parameters));
+  $: setContext('events', $events);
 </script>
 
 <section class="flex flex-col gap-4">
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
index ca6a5066..c102b577 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/compact/__layout.svelte
@@ -1,30 +1,15 @@
-<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 { getContext } from 'svelte';
   import { Activities } from '$lib/models/activity';
   import EventTable from '$lib/components/event-table.svelte';
 
-  export let events: HistoryEventWithId[];
-
-  let activities = new Activities(events);
+  export let events = getContext<EventualHistoryEvents>('events');
 </script>
 
-<EventTable events={activities}>
-  <div slot="details" class="w-full h-full py-4">
-    <slot />
-  </div>
-</EventTable>
+{#await Activities.fromPromise(events) then activities}
+  <EventTable events={activities}>
+    <div slot="details" class="w-full h-full py-4">
+      <slot />
+    </div>
+  </EventTable>
+{/await}
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
index 1cce372a..2a7a4898 100644
--- 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
@@ -2,7 +2,7 @@
   import type { EventParameter } from '$lib/utilities/route-for';
   import type { LoadInput } from '@sveltejs/kit';
 
-  export async function load({ page, stuff }: LoadInput) {
+  export async function load({ page }: LoadInput) {
     const {
       workflow: workflowId,
       run: runId,
@@ -10,13 +10,8 @@
       eventId,
     } = page.params;
 
-    const { events } = stuff as {
-      events: HistoryEventWithId[];
-    };
-
     return {
       props: {
-        events,
         params: {
           workflowId,
           runId,
@@ -29,17 +24,29 @@
 </script>
 
 <script lang="ts">
+  import { getContext } from 'svelte';
+
   import { Activities } from '$lib/models/activity';
   import { routeFor } from '$lib/utilities/route-for';
   import { page } from '$app/stores';
 
-  export let events: HistoryEventWithId[];
+  let events = getContext<EventualHistoryEvents>('events');
   export let params: EventParameter;
 
-  let activities = new Activities(events);
-  let activity = activities.get(params.eventId);
+  const getActivity = async (
+    events: EventualHistoryEvents,
+    id: string,
+  ): Promise<{ activity: Activity; events: HistoryEventWithId[] }> => {
+    const activities = await Activities.fromPromise(events);
+    const activity = activities.get(id);
+
+    return {
+      activity,
+      events: activity.events,
+    };
+  };
 
-  const getHref = (event: HistoryEventWithId) =>
+  const getHref = (activity, event: HistoryEventWithId) =>
     routeFor('workflow.events.compact.activity.event', {
       ...params,
       activityId: activity.id,
@@ -50,13 +57,18 @@
 <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}
+      {#await getActivity(events, params.eventId) then { activity, events }}
+        {#each events as event}
+          <li>
+            <a
+              href={getHref(activity, event)}
+              class:active={$page.path === getHref(activity, event)}
+            >
+              {event.eventType}
+            </a>
+          </li>
+        {/each}
+      {/await}
     </ul>
   </nav>
   <slot />
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
index 5cc6c9fb..a42761aa 100644
--- 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
@@ -1,23 +1,20 @@
-<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 { getContext } from 'svelte';
+  import { page } from '$app/stores';
+
   import EventDetails from '$lib/components/event-details.svelte';
 
-  export let event: HistoryEventWithId;
+  const findEvent = async (
+    data: EventualHistoryEvents,
+    id: string,
+  ): Promise<HistoryEventWithId> => {
+    return data.then((events) => events.find((event) => event.id === id));
+  };
+
+  let events = getContext<EventualHistoryEvents>('events');
+  $: event = findEvent(events, $page.params.id);
 </script>
 
-<EventDetails attributes={event.attributes} />
+{#await event then { attributes }}
+  <EventDetails {attributes} />
+{/await}
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
index 535c8087..c8beded7 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/full/__layout.svelte
@@ -1,51 +1,37 @@
-<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[];
-    };
+<script lang="ts">
+  import { getContext } from 'svelte';
 
-    return {
-      props: {
-        events,
-        pendingActivities: workflow.pendingActivities,
-      },
-    };
-  }
-</script>
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
 
-<script lang="ts">
-  import { eventTypeInCategory } from '$lib/utilities/get-event-categorization';
+  import { getVisibleEvents } from '$lib/utilities/get-visible-events';
 
   import FilterSelect from '$lib/components/select/filter-select.svelte';
   import Option from '$lib/components/select/option.svelte';
   import EventTable from '$lib/components/event-table.svelte';
 
-  export let events: HistoryEventWithId[];
-  export let pendingActivities: PendingActivity[];
-
   let category: EventTypeCategory = null;
 
-  $: visibleEvents = events.filter(eventTypeInCategory(category));
-  $: eventsAndActivities = [...pendingActivities, ...visibleEvents];
+  let workflow = getContext<PromiseLike<WorkflowExecution>>('workflow');
+  let events = getContext<EventualHistoryEvents>('events');
+
+  $: eventsAndActivities = getVisibleEvents(events, workflow, category);
 </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>
+{#await eventsAndActivities then events}
+  <EventTable {events}>
+    <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>
+{/await}
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
index 5cc6c9fb..a42761aa 100644
--- 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
@@ -1,23 +1,20 @@
-<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 { getContext } from 'svelte';
+  import { page } from '$app/stores';
+
   import EventDetails from '$lib/components/event-details.svelte';
 
-  export let event: HistoryEventWithId;
+  const findEvent = async (
+    data: EventualHistoryEvents,
+    id: string,
+  ): Promise<HistoryEventWithId> => {
+    return data.then((events) => events.find((event) => event.id === id));
+  };
+
+  let events = getContext<EventualHistoryEvents>('events');
+  $: event = findEvent(events, $page.params.id);
 </script>
 
-<EventDetails attributes={event.attributes} />
+{#await event then { attributes }}
+  <EventDetails {attributes} />
+{/await}
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
index 68b8a983..daca085a 100644
--- 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
@@ -1,28 +1,23 @@
-<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 { getContext } from 'svelte';
+  import { page } from '$app/stores';
   import EventDetails from '$lib/components/event-details.svelte';
 
-  export let event: PendingActivity;
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
+  const findActivity = async (
+    workflow: PromiseLike<WorkflowExecution>,
+    id: string,
+  ): Promise<PendingActivity> => {
+    return workflow.then(({ pendingActivities }) => {
+      return pendingActivities.find((activity) => activity.activityId === id);
+    });
+  };
+
+  let workflow = getContext<PromiseLike<WorkflowExecution>>('workflow');
+  $: pendingActivity = findActivity(workflow, $page.params.id);
 </script>
 
-<EventDetails attributes={event} />
+{#await pendingActivity then activity}
+  <EventDetails attributes={activity} />
+{/await}
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json.svelte
new file mode 100644
index 00000000..9004e356
--- /dev/null
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+  import { getContext } from 'svelte';
+  import CodeBlock from '$lib/components/code-block.svelte';
+
+  let events = getContext<EventualHistoryEvents>('events');
+</script>
+
+{#await events then content}
+  <CodeBlock {content} />
+{/await}
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
deleted file mode 100644
index c6a74701..00000000
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/json/index.svelte
+++ /dev/null
@@ -1,22 +0,0 @@
-<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]/workers.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte
index af4c2e00..8edeb9c9 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/workers.svelte
@@ -2,30 +2,42 @@
   import type { LoadInput } from '@sveltejs/kit';
 
   import { getPollers } from '$lib/services/pollers-service';
-  export async function load({ fetch, page }: LoadInput) {
+
+  export async function load({ page }: LoadInput) {
     const { namespace } = page.params;
-    const queue = page.query.get('queue');
 
-    return await getPollers({ queue, namespace }, fetch).then((pollers) => ({
-      props: { pollers },
-    }));
+    return {
+      props: { namespace },
+    };
   }
 </script>
 
 <script lang="ts">
+  import { getContext } from 'svelte';
+  import { refreshable } from '$lib/stores/refreshable';
+
+  import type { WorkflowExecution } from '$lib/models/workflow-execution';
+
   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;
+  export let namespace: string;
+
+  $: pollers = refreshable(async () => {
+    let workflow = await getContext<PromiseLike<WorkflowExecution>>('workflow');
+    return getPollers({ queue: workflow.taskQueue, namespace });
+  });
 </script>
 
 <section>
-  {#each pollers.pollers as poller}
-    <WorkersTable>
-      <WorkersRow {poller} />
-    </WorkersTable>
-  {:else}
-    <EmptyState title={'No Workers Found'} />
-  {/each}
+  {#await $pollers then workers}
+    {#each workers.pollers as poller}
+      <WorkersTable>
+        <WorkersRow {poller} />
+      </WorkersTable>
+    {:else}
+      <EmptyState title={'No Workers Found'} />
+    {/each}
+  {/await}
 </section>


Test patch:
diff --git a/src/lib/utilities/route-for.test.ts b/src/lib/utilities/route-for.test.ts
index 69587b65..8e8ac432 100644
--- a/src/lib/utilities/route-for.test.ts
+++ b/src/lib/utilities/route-for.test.ts
@@ -108,8 +108,9 @@ describe(routeFor, () => {
   it('should route to "workers"', () => {
     const path = routeFor('workers', {
       namespace: 'default',
-      queue: 'rainbow-statuses',
+      workflowId: 'abc',
+      runId: 'def',
     });
-    expect(path).toBe('/namespaces/default/workers/rainbow-statuses');
+    expect(path).toBe('/namespaces/default/workflows/abc/def/workers');
   });
 });