Repository: temporalio/ui
Pull request #414 (2022-04-11 18:24:15)

Use a list of preferred event attributes
This change checks a list of preferred activity types to display in the summary view. If none exists, then it falls back to using the first one.

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>f8c052d</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://9c4035fb.ui-c3x.pages.dev'>https://9c4035fb.ui-c3x.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/temporal-pizza/9c4035fb-a78e-4731-9328-de6f165c1c5e)


Patch:
diff --git a/README.md b/README.md
index 57c85ace..6c703e62 100644
--- a/README.md
+++ b/README.md
@@ -60,9 +60,9 @@ Set these environment variables if you want to change their defaults
 
 ## Developing with Canary
 
-To get a better representation of production data, you can run our UI with the go-canary repo. You will need go installed on your machine.
+To get a better representation of production data, you can run our UI with the [canary-go](https://github.com/temporalio/canary-go) repo. You will need go installed on your machine.
 
-### go-canary
+### canary-go
 
 ```bash
 make bins
diff --git a/src/lib/components/event/view/event-summary.svelte b/src/lib/components/event/view/event-summary.svelte
index f928dc3b..0e05020a 100644
--- a/src/lib/components/event/view/event-summary.svelte
+++ b/src/lib/components/event/view/event-summary.svelte
@@ -1,6 +1,4 @@
 <script lang="ts">
-  import { page } from '$app/stores';
-
   import LoadingRow from '$lib/components/loading-row.svelte';
   import Pagination from '$lib/components/pagination.svelte';
 
diff --git a/src/lib/utilities/get-namespace.ts b/src/lib/utilities/get-namespace.ts
index e014fecc..d73aaec0 100644
--- a/src/lib/utilities/get-namespace.ts
+++ b/src/lib/utilities/get-namespace.ts
@@ -6,6 +6,11 @@ type GetNamespaceParameters = {
   namespaces: DescribeNamespaceResponse[];
 };
 
+type GetDefaultNamespaceParameters = {
+  namespaces: DescribeNamespaceResponse[];
+  settings: Settings;
+};
+
 export const getNamespace = ({
   namespace,
   defaultNamespace,
@@ -21,3 +26,25 @@ export const getNamespace = ({
 
   return defaultNamespace;
 };
+
+export const getDefaultNamespace = ({
+  namespaces,
+  settings,
+}: GetDefaultNamespaceParameters): string => {
+  const { showTemporalSystemNamespace, defaultNamespace } = settings;
+
+  const namespaceNames = namespaces
+    .map(
+      (namespace: DescribeNamespaceResponse) => namespace?.namespaceInfo?.name,
+    )
+    .filter(
+      (namespace: string) =>
+        showTemporalSystemNamespace || namespace !== 'temporal-system',
+    );
+
+  return (
+    namespaceNames.find((ns) => ns === defaultNamespace) ??
+    namespaceNames[0] ??
+    defaultNamespace
+  );
+};
diff --git a/src/lib/utilities/get-single-attribute-for-event.ts b/src/lib/utilities/get-single-attribute-for-event.ts
index c6a975fc..5cdb6b6f 100644
--- a/src/lib/utilities/get-single-attribute-for-event.ts
+++ b/src/lib/utilities/get-single-attribute-for-event.ts
@@ -1,3 +1,7 @@
+type SummaryAttribute = { key: string; value: string };
+
+const emptyAttribute: SummaryAttribute = { key: '', value: '' };
+
 export const shouldDisplayAttribute = (
   key: string,
   value: unknown,
@@ -10,32 +14,82 @@ export const shouldDisplayAttribute = (
   return true;
 };
 
-export const shouldDisplayAsWorkflowLink = (key: string) => {
-  if (!key) return false;
+const keysWithWorkflowLinks = [
+  'baseRunId',
+  'continuedExecutionRunId',
+  'firstExecutionRunId',
+  'newExecutionRunId',
+  'newRunId',
+  'originalExecutionRunId',
+] as const;
 
-  if (key.toLowerCase().endsWith('runid')) {
-    return true;
+export const shouldDisplayAsWorkflowLink = (
+  key: string,
+): key is typeof keysWithWorkflowLinks[number] => {
+  for (const workflowKey of keysWithWorkflowLinks) {
+    if (key === workflowKey) return true;
   }
 
   return false;
 };
 
-const mapObjectToPropertValue = (key: string, value: any) => {
-  const firstKey = Object.keys(value)[0];
-  return { key: `${key}.${firstKey}`, value: value[firstKey] };
+const formatSummaryValue = (key: string, value: unknown): SummaryAttribute => {
+  if (typeof value === 'object') {
+    const [firstKey] = Object.keys(value);
+    return { key: `${key}.${firstKey}`, value: value[firstKey] };
+  } else {
+    return { key, value: value.toString() };
+  }
 };
 
-const getFirstDisplayAttribute = (attributes: any) => {
-  if (!attributes) return;
+/**
+ * A list of the keys that should be shown in the summary view.
+ */
+const preferredSummaryKeys = ['activityType'] as const;
+
+/**
+ * Returns that first event attribute that is eligible to be displayed.
+ */
+const getFirstDisplayAttribute = ({
+  attributes,
+}: HistoryEventWithId): { key: string; value: string } => {
   for (const [key, value] of Object.entries(attributes)) {
     if (shouldDisplayAttribute(key, value)) {
-      if (typeof value === 'object') {
-        return mapObjectToPropertValue(key, value);
-      } else {
-        return { key, value: value.toString() };
-      }
+      return formatSummaryValue(key, value);
+    }
+  }
+};
+
+/**
+ * Iterates through the keys of an event and compares it with the list of
+ * preferred keys. If a preferred key is found, it will be returned.
+ * Otherwise, it will return the first eligible event attribute.
+ */
+const getPreferredSummaryAttribute = (
+  event: HistoryEventWithId,
+): SummaryAttribute => {
+  const first = getFirstDisplayAttribute(event);
+
+  for (const [key, value] of Object.entries(event.attributes)) {
+    for (const preferredKey of preferredSummaryKeys) {
+      if (key === preferredKey) return formatSummaryValue(key, value);
     }
   }
+
+  return first;
+};
+
+const getSummaryForEventGroup = (
+  eventGroup: CompactEventGroup,
+): SummaryAttribute => {
+  let input = '';
+  const attributes = eventGroup.initialEvent.attributes;
+
+  for (const [key, value] of Object.entries(attributes)) {
+    if (key === 'input') input = value;
+  }
+
+  return { key: 'input', value: input };
 };
 
 export const getSingleAttributeForEvent = ({
@@ -44,12 +98,12 @@ export const getSingleAttributeForEvent = ({
 }: {
   event: HistoryEventWithId | null;
   eventGroup: CompactEventGroup | null;
-}): { key: string; value: string } => {
+}): SummaryAttribute => {
+  if (!event && !eventGroup) return emptyAttribute;
+
   if (eventGroup) {
-    const attributes = eventGroup?.initialEvent?.attributes as any;
-    const input = attributes?.input ?? '';
-    return { key: 'input', value: input };
+    return getSummaryForEventGroup(eventGroup);
   }
 
-  return getFirstDisplayAttribute(event?.attributes) ?? { key: '', value: '' };
+  return getPreferredSummaryAttribute(event);
 };
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index b54c3cc0..b2ce3e1f 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -9,6 +9,7 @@
   import { fetchUser } from '$lib/services/user-service';
   import { fetchCluster } from '$lib/services/cluster-service';
   import { fetchNamespaces } from '$lib/services/namespaces-service';
+  import { getDefaultNamespace } from '$lib/utilities/get-namespace';
 
   export const load: Load = async function ({ url, fetch }) {
     const settings: Settings = await fetchSettings({ url }, fetch);
@@ -18,12 +19,13 @@
       fetch,
     );
 
+    const defaultNamespace = getDefaultNamespace({ namespaces, settings });
     const user = await fetchUser(fetch);
     const cluster = await fetchCluster(settings, fetch);
 
     return {
-      props: { namespaces, user, cluster },
-      stuff: { namespaces, settings },
+      props: { user, cluster },
+      stuff: { namespaces, settings: { ...settings, defaultNamespace } },
     };
   };
 </script>
diff --git a/src/routes/_header.svelte b/src/routes/_header.svelte
index b75dbf40..c5c35876 100644
--- a/src/routes/_header.svelte
+++ b/src/routes/_header.svelte
@@ -11,7 +11,8 @@
   import NavigationLink from './_navigation-link.svelte';
   import IsCloudGuard from '$lib/components/is-cloud-guard.svelte';
 
-  $: namespace = $page.params.namespace;
+  $: namespace =
+    $page.params.namespace || $page.stuff?.settings?.defaultNamespace;
 
   export let user: User;
 </script>
diff --git a/src/routes/import/events/history/index.svelte b/src/routes/import/events/history/index.svelte
index 419ae9d6..f1d4add2 100644
--- a/src/routes/import/events/history/index.svelte
+++ b/src/routes/import/events/history/index.svelte
@@ -2,7 +2,7 @@
   import type { Load } from '@sveltejs/kit';
   import { routeForImport } from '$lib/utilities/route-for';
 
-  export const load: Load = async function ({ url }) {
+  export const load: Load = async function () {
     const redirect = routeForImport({ importType: 'events', view: 'summary' });
     return { status: 302, redirect };
   };
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
index 903e1ff0..630fcbbb 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_pending-activties.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/history/_pending-activties.svelte
@@ -33,7 +33,7 @@
               <h4>Activity Name</h4>
               <p>
                 <span class="bg-gray-300 text-gray-700 px-2">
-                  {pendingActivity.activityType?.name}
+                  {pendingActivity.activityType}
                 </span>
               </p>
             </div>


Test patch:
diff --git a/src/lib/utilities/get-namespace.test.ts b/src/lib/utilities/get-namespace.test.ts
new file mode 100644
index 00000000..9b256c78
--- /dev/null
+++ b/src/lib/utilities/get-namespace.test.ts
@@ -0,0 +1,172 @@
+import { getDefaultNamespace } from './get-namespace';
+
+const temporalSystemNamespace = {
+  namespaceInfo: {
+    name: 'temporal-system',
+    state: 'Registered',
+    description: 'Temporal internal system namespace',
+    ownerEmail: 'temporal-core@temporal.io',
+    data: {},
+    id: '32049b68-7872-4094-8e63-d0dd59896a83',
+  },
+  config: {
+    workflowExecutionRetentionTtl: '604800s',
+    badBinaries: {
+      binaries: {},
+    },
+    historyArchivalState: 'Disabled',
+    historyArchivalUri: '',
+    visibilityArchivalState: 'Disabled',
+    visibilityArchivalUri: '',
+  },
+  replicationConfig: {
+    activeClusterName: 'active',
+    clusters: [
+      {
+        clusterName: 'active',
+      },
+    ],
+    state: 'Unspecified',
+  },
+  failoverVersion: '0',
+  isGlobalNamespace: false,
+};
+
+const canaryNamespace = {
+  namespaceInfo: {
+    name: 'canary',
+    state: 'Registered',
+    description: 'Namespace for running temporal canary workflows',
+    ownerEmail: 'canary',
+    data: {},
+    id: '6879d26a-d9ad-486b-a572-b11aeed816e8',
+  },
+  config: {
+    workflowExecutionRetentionTtl: '864000s',
+    badBinaries: {
+      binaries: {},
+    },
+    historyArchivalState: 'Disabled',
+    historyArchivalUri: '',
+    visibilityArchivalState: 'Disabled',
+    visibilityArchivalUri: '',
+  },
+  replicationConfig: {
+    activeClusterName: 'active',
+    clusters: [
+      {
+        clusterName: 'active',
+      },
+    ],
+    state: 'Unspecified',
+  },
+  failoverVersion: '0',
+  isGlobalNamespace: false,
+};
+
+const defaultNamespace = {
+  namespaceInfo: {
+    name: 'default',
+    state: 'Registered',
+    description: 'Default namespace',
+    ownerEmail: 'default',
+    data: {},
+    id: '6879d26a-d9ad-486b-a572-b11aeed816e8',
+  },
+  config: {
+    workflowExecutionRetentionTtl: '864000s',
+    badBinaries: {
+      binaries: {},
+    },
+    historyArchivalState: 'Disabled',
+    historyArchivalUri: '',
+    visibilityArchivalState: 'Disabled',
+    visibilityArchivalUri: '',
+  },
+  replicationConfig: {
+    activeClusterName: 'active',
+    clusters: [
+      {
+        clusterName: 'active',
+      },
+    ],
+    state: 'Unspecified',
+  },
+  failoverVersion: '0',
+  isGlobalNamespace: false,
+};
+
+const getSettings = (
+  defaultNamespace: string,
+  showTemporalSystemNamespace: boolean,
+) => {
+  return {
+    auth: {
+      enabled: false,
+      options: ['organization', 'invitation', 'audience'],
+    },
+    defaultNamespace,
+    showTemporalSystemNamespace,
+  };
+};
+
+describe(getDefaultNamespace, () => {
+  it('should return default namespace if it exists in namespaces and matches defaultNamespace setting', () => {
+    const namespaces = [defaultNamespace];
+    const settings = getSettings('default', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('default');
+  });
+  it('should return default namespace if it exists in namespaces and no defaultNamespace setting', () => {
+    const namespaces = [defaultNamespace];
+    const settings = getSettings('', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('default');
+  });
+  it('should return default namespace if it exists in namespaces and includes temporal system namespace with !ShowTemporalSystemNamespace', () => {
+    const namespaces = [temporalSystemNamespace, defaultNamespace];
+    const settings = getSettings('', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('default');
+  });
+  it('should return default namespace if it exists in namespaces and includes temporal system namespace with ShowTemporalSystemNamespace', () => {
+    const namespaces = [temporalSystemNamespace, defaultNamespace];
+    const settings = getSettings('', true);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual(
+      'temporal-system',
+    );
+  });
+  it('should return canary namespace if single namespace that does not match defaultNamespace setting', () => {
+    const namespaces = [canaryNamespace];
+    const settings = getSettings('default', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('canary');
+  });
+  it('should return default namespace if multiple namespaces and matching defaultNamespace setting', () => {
+    const namespaces = [canaryNamespace, defaultNamespace];
+    const settings = getSettings('default', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('default');
+  });
+  it('should return canary namespace if multiple namespaces and matching defaultNamespace setting', () => {
+    const namespaces = [canaryNamespace, defaultNamespace];
+    const settings = getSettings('canary', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('canary');
+  });
+  it('should return first namespace if multiple namespaces and no defaultNamespace setting', () => {
+    const namespaces = [canaryNamespace, defaultNamespace];
+    const settings = getSettings('', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('canary');
+  });
+  it('should return first namespace if multiple namespaces and no defaultNamespace setting and including ShowTemporalSystemNamespace', () => {
+    const namespaces = [
+      temporalSystemNamespace,
+      canaryNamespace,
+      defaultNamespace,
+    ];
+    const settings = getSettings('', true);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual(
+      'temporal-system',
+    );
+  });
+  it('should return defaultNamespace setting if no namespaces', () => {
+    const namespaces = [];
+    const settings = getSettings('default', false);
+    expect(getDefaultNamespace({ namespaces, settings })).toEqual('default');
+  });
+});
diff --git a/src/lib/utilities/get-single-attribute-for-event.test.ts b/src/lib/utilities/get-single-attribute-for-event.test.ts
index 61c68783..d9b1ac13 100644
--- a/src/lib/utilities/get-single-attribute-for-event.test.ts
+++ b/src/lib/utilities/get-single-attribute-for-event.test.ts
@@ -168,11 +168,6 @@ describe(shouldDisplayAsWorkflowLink, () => {
     expect(shouldDisplayAsWorkflowLink('continuedExecutionRunId')).toBe(true);
   });
 
-  it('should be case insensitive', () => {
-    expect(shouldDisplayAsWorkflowLink('originalExecutionRunId')).toBe(true);
-    expect(shouldDisplayAsWorkflowLink('OriginalExecutionrunid')).toBe(true);
-  });
-
   it('should return false for event keys that do not end with RunId', () => {
     expect(shouldDisplayAsWorkflowLink('')).toBe(false);
     expect(shouldDisplayAsWorkflowLink('workflowType.name')).toBe(false);