Repository: temporalio/ui
Pull request #160 (2021-12-08 19:43:54)

Add colored labels for event types
Adds code highlighting for code snippets as well as classification and labels for event types.

![image](https://user-images.githubusercontent.com/251000/145273631-bad3913d-cc5e-4cf6-864e-90f5113eda6b.png)



Patch:
diff --git a/src/lib/components/code-block.svelte b/src/lib/components/code-block.svelte
index eab2885b..3e7d8371 100644
--- a/src/lib/components/code-block.svelte
+++ b/src/lib/components/code-block.svelte
@@ -6,6 +6,7 @@
   export let heading = '';
   export let content: string | Parameters<typeof JSON.stringify>[0];
   export let copied = false;
+  export let inline = false;
 
   const copy = () =>
     navigator.clipboard
@@ -18,7 +19,7 @@
 
   const formatJSON = (jsonData: string) => {
     const parsedData = JSON.parse(jsonData);
-    const formated = JSON.stringify(parsedData, undefined, 4);
+    const formated = JSON.stringify(parsedData, undefined, 2);
     return formated;
   };
 
@@ -26,30 +27,38 @@
 </script>
 
 {#if content}
-  <div class="relative group w-full mb-2">
-    <div id="clipboard" />
+  <div class="relative w-full">
+    {#if !inline}<div id="clipboard" />{/if}
 
     {#if heading}
       <h3 class="text-lg mb-2 w-full">{heading}</h3>
     {/if}
-    <pre
-      class="p-4">
-      <code class="language-json">
-        {formatJSON(JSON.stringify(content))}
+    {#if inline}
+      <code class="px-2 block language-json overflow-hidden">
+        {JSON.stringify(content)}
       </code>
-    </pre>
-    <button on:click={copy}>
-      {#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"
-        />
-      {: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"
-        />
-      {/if}
-    </button>
+    {:else}
+      <pre
+        class="p-4">
+        <code class="language-json">
+          {formatJSON(JSON.stringify(content))}
+        </code>
+      </pre>
+    {/if}
+    {#if !inline}
+      <button on:click={copy}>
+        {#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"
+          />
+        {: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"
+          />
+        {/if}
+      </button>
+    {/if}
   </div>
 {/if}
diff --git a/src/lib/utilities/get-event-classification.ts b/src/lib/utilities/get-event-classification.ts
new file mode 100644
index 00000000..c04602f3
--- /dev/null
+++ b/src/lib/utilities/get-event-classification.ts
@@ -0,0 +1,27 @@
+const eventClassifications = [
+  'Scheduled',
+  'Open',
+  'New',
+  'Started',
+  'Initiated',
+  'Running',
+  'Completed',
+  'Fired',
+  'CancelRequested',
+  'TimedOut',
+  'Signaled',
+  'Canceled',
+  'Failed',
+  'Terminated',
+];
+
+export const getEventClassification = (
+  event: HistoryEvent,
+): typeof eventClassifications[number] => {
+  const eventType = event.eventType.toString();
+
+  if (eventType.includes('RequestCancel')) return 'CancelRequested';
+  for (const classification of eventClassifications) {
+    if (eventType.includes(classification)) return classification;
+  }
+};
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte
index 93642771..d10b65cc 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/_event.svelte
@@ -1,41 +1,73 @@
 <script lang="ts">
   import type { HistoryEvent } from '$types';
   import { getAttributesFromEvent } from '$lib/utilities/get-attributes-from-event';
+  import { getEventClassification } from '$lib/utilities/get-event-classification';
+
+  import CodeBlock from '$lib/components/code-block.svelte';
 
   export let event: HistoryEvent;
   const summaryEvent = getAttributesFromEvent(event);
 </script>
 
-<div class="flex flex-row event-box border-2 p-4 rounded-lg justify-between">
-  <h2>{event.eventType}</h2>
-  <div class="flex flex-row items-center event">
+<article class="flex flex-row items-start event-box border-2 p-4 rounded-lg">
+  <h2 class="w-1/3 flex-1 {event.eventType}">
+    <span class="label {getEventClassification(event)}">{event.eventType}</span>
+  </h2>
+  <div class="flex flex-row items-center event gap-4 w-full flex-3">
     {#each Object.entries(summaryEvent.attributes) as [event, value]}
-      {#if value}
-        {#if typeof value === 'object'}
-          <p>{event} <span>{JSON.stringify(value)}</span></p>
-        {:else}
-          <p>{event} <span>{value}</span></p>
-        {/if}
+      {#if typeof value === 'object'}
+        <div class="flex flex-nowrap flex-row gap-2">
+          <h4>{event}</h4>
+          <CodeBlock content={value} inline={true} />
+        </div>
+      {:else if value}
+        <div class="flex gap-2">
+          <h4>{event}</h4>
+          <p class="label">{value}</p>
+        </div>
       {/if}
     {/each}
   </div>
-</div>
+</article>
 
 <style lang="postcss">
   .event-box {
     margin: 1rem 0;
   }
 
-  .event {
-    width: 70%;
+  .label {
+    @apply bg-gray-300 px-2 rounded-sm;
+  }
+
+  .Scheduled {
+    @apply text-gray-700 bg-gray-100;
+  }
+
+  .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;
   }
 
-  span {
-    @apply bg-gray-300 p-1;
+  .CancelRequested,
+  .TimedOut,
+  .Signaled,
+  .Cancelled {
+    @apply text-yellow-700 bg-yellow-100;
   }
 
-  p {
-    @apply text-sm;
-    margin: 0 1rem;
+  .Failed,
+  .Terminated {
+    @apply text-red-700 bg-red-100;
   }
 </style>


Test patch:
diff --git a/src/lib/utilities/get-event-classification.test.ts b/src/lib/utilities/get-event-classification.test.ts
new file mode 100644
index 00000000..3d8921ab
--- /dev/null
+++ b/src/lib/utilities/get-event-classification.test.ts
@@ -0,0 +1,277 @@
+import { getEventClassification } from './get-event-classification';
+
+describe(getEventClassification, () => {
+  it('should return "Started" for WorkflowExecutionStartedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionStartedEvent' }),
+    ).toBe('Started');
+  });
+
+  it('should return "Completed" for WorkflowExecutionCompletedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionCompletedEvent' }),
+    ).toBe('Completed');
+  });
+
+  it('should return "Failed" for WorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionFailedEvent' }),
+    ).toBe('Failed');
+  });
+
+  it('should return "TimedOut" for WorkflowExecutionTimedOutEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionTimedOutEvent' }),
+    ).toBe('TimedOut');
+  });
+
+  it('should return "Scheduled" for WorkflowTaskScheduledEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowTaskScheduledEvent' }),
+    ).toBe('Scheduled');
+  });
+
+  it('should return "Started" for WorkflowTaskStartedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowTaskStartedEvent' }),
+    ).toBe('Started');
+  });
+
+  it('should return "Completed" for WorkflowTaskCompletedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowTaskCompletedEvent' }),
+    ).toBe('Completed');
+  });
+
+  it('should return "TimedOut" for WorkflowTaskTimedOutEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowTaskTimedOutEvent' }),
+    ).toBe('TimedOut');
+  });
+
+  it('should return "Failed" for WorkflowTaskFailedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowTaskFailedEvent' }),
+    ).toBe('Failed');
+  });
+
+  it('should return "Scheduled" for ActivityTaskScheduledEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskScheduledEvent' }),
+    ).toBe('Scheduled');
+  });
+
+  it('should return "Started" for ActivityTaskStartedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskStartedEvent' }),
+    ).toBe('Started');
+  });
+
+  it('should return "Completed" for ActivityTaskCompletedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskCompletedEvent' }),
+    ).toBe('Completed');
+  });
+
+  it('should return "Failed" for ActivityTaskFailedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskFailedEvent' }),
+    ).toBe('Failed');
+  });
+
+  it('should return "TimedOut" for ActivityTaskTimedOutEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskTimedOutEvent' }),
+    ).toBe('TimedOut');
+  });
+
+  it('should return "Started" for TimerStartedEven', () => {
+    expect(getEventClassification({ eventType: 'TimerStartedEven' })).toBe(
+      'Started',
+    );
+  });
+
+  it('should return "Fired" for TimerFiredEven', () => {
+    expect(getEventClassification({ eventType: 'TimerFiredEven' })).toBe(
+      'Fired',
+    );
+  });
+
+  it('should return "CancelRequested" for ActivityTaskCancelRequestedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskCancelRequestedEvent' }),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "Canceled" for ActivityTaskCanceledEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'ActivityTaskCanceledEvent' }),
+    ).toBe('Canceled');
+  });
+
+  it('should return "Canceled" for TimerCanceledEvent', () => {
+    expect(getEventClassification({ eventType: 'TimerCanceledEvent' })).toBe(
+      'Canceled',
+    );
+  });
+
+  it('should return undefined for MarkerRecordedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'MarkerRecordedEvent' }),
+    ).toBeUndefined();
+  });
+
+  it('should return "Signaled" for WorkflowExecutionSignaledEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionSignaledEvent' }),
+    ).toBe('Signaled');
+  });
+
+  it('should return "Terminated" for WorkflowExecutionTerminatedEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionTerminatedEvent' }),
+    ).toBe('Terminated');
+  });
+
+  it('should return "CancelRequested" for WorkflowExecutionCancelRequestedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'WorkflowExecutionCancelRequestedEvent',
+      }),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "Canceled" for WorkflowExecutionCanceledEvent', () => {
+    expect(
+      getEventClassification({ eventType: 'WorkflowExecutionCanceledEvent' }),
+    ).toBe('Canceled');
+  });
+
+  it('should return "$x" for RequestCancelExternalWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'RequestCancelExternalWorkflowExecutionInitiatedEvent',
+      }),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "$x" for RequestCancelExternalWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'RequestCancelExternalWorkflowExecutionFailedEvent',
+      }),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "CancelRequested" for ExternalWorkflowExecutionCancelRequestedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ExternalWorkflowExecutionCancelRequestedEvent',
+      }),
+    ).toBe('CancelRequested');
+  });
+
+  it('should return "New" for WorkflowExecutionContinuedAsNewEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'WorkflowExecutionContinuedAsNewEvent',
+      }),
+    ).toBe('New');
+  });
+
+  it('should return "$x" for StartChildWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'StartChildWorkflowExecutionInitiatedEvent',
+      }),
+    ).toBe('Initiated');
+  });
+
+  it('should return "Failed" for StartChildWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'StartChildWorkflowExecutionFailedEvent',
+      }),
+    ).toBe('Failed');
+  });
+
+  it('should return "Started" for ChildWorkflowExecutionStartedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionStartedEvent',
+      }),
+    ).toBe('Started');
+  });
+
+  it('should return "Completed" for ChildWorkflowExecutionCompletedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionCompletedEvent',
+      }),
+    ).toBe('Completed');
+  });
+
+  it('should return "Failed" for ChildWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionFailedEvent',
+      }),
+    ).toBe('Failed');
+  });
+
+  it('should return "Canceled" for ChildWorkflowExecutionCanceledEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionCanceledEvent',
+      }),
+    ).toBe('Canceled');
+  });
+
+  it('should return "TimedOut" for ChildWorkflowExecutionTimedOutEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionTimedOutEvent',
+      }),
+    ).toBe('TimedOut');
+  });
+
+  it('should return "Terminated" for ChildWorkflowExecutionTerminatedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ChildWorkflowExecutionTerminatedEvent',
+      }),
+    ).toBe('Terminated');
+  });
+
+  it('should return "Initiated" for SignalExternalWorkflowExecutionInitiatedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'SignalExternalWorkflowExecutionInitiatedEvent',
+      }),
+    ).toBe('Initiated');
+  });
+
+  it('should return "Failed" for SignalExternalWorkflowExecutionFailedEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'SignalExternalWorkflowExecutionFailedEvent',
+      }),
+    ).toBe('Failed');
+  });
+
+  it('should return "Signaled" for ExternalWorkflowExecutionSignaledEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'ExternalWorkflowExecutionSignaledEvent',
+      }),
+    ).toBe('Signaled');
+  });
+
+  it('should return undefined for UpsertWorkflowSearchAttributesEvent', () => {
+    expect(
+      getEventClassification({
+        eventType: 'UpsertWorkflowSearchAttributesEvent',
+      }),
+    ).toBeUndefined();
+  });
+});