Repository: temporalio/ui
Pull request #398 (2022-04-06 06:35:40)

Make RunID keys in History navigate to worklow page
<!--- Note to EXTERNAL Contributors -->
<!-- Thanks for opening a PR! 
If it is a significant code change, please **make sure there is an open issue** for this. 
We work best with you when we have accepted the idea first before you code. -->

<!--- For ALL Contributors 👇 -->

## What was changed
<!-- Describe what has changed in this PR -->

Run IDs in History are now links to workflow pages

![image](https://user-images.githubusercontent.com/11838981/161910594-0e7a9686-dd0a-4757-ad1a-8cff2a90ba02.png)

## Why?
<!-- Tell your future self why have you made these changes -->

Helps to quickly navigate to Continued as new execution runs or find the previous run

addressed #377 

## Checklist
<!--- add/delete as needed --->

1. Closes <!-- add issue number here -->

2. How was this tested:
<!--- Please describe how you tested your changes/how we can test them -->

Unit test + manual

3. Any docs updates needed?
<!--- update README if applicable
      or point out where to update docs.temporal.io -->


cypress[bot]: 
<p align="center">
  <br />
  <img src="https://assets.cypress.io/github-pr-comment-icons/cypress-logo.png" height="32" />
</p>

---

### Test summary
<a href="https://dashboard.cypress.io/projects/g7xfxw/runs/69/overview?utm_source=github"><img src="https://assets.cypress.io/github-pr-comment-icons/green-check-mark.png" width="14" height="14" title="Passed" /></a> <b title="Passed">26</b>  &bull; <a href="https://dashboard.cypress.io/projects/g7xfxw/runs/69/test-results?utm_source=github&statuses=%5B%7B%22value%22%3A%22FAILED%22%2C%22label%22%3A%22FAILED%22%7D%5D"><img src="https://assets.cypress.io/github-pr-comment-icons/red-x.png" width="14" height="14" title="Failed" /></a> <b title="Failed">0</b>  &bull; <img src="https://assets.cypress.io/github-pr-comment-icons/gray-circle.png" width="14" height="14" title="Tests that did not run due to a developer annotating a test with .skip" /> <b title="Pending">0</b> &bull; <img src="https://assets.cypress.io/github-pr-comment-icons/yellow-triangle.png" width="14" height="14" title="Tests that did not run due to a failure in a mocha hook" /> <b title="Skipped">0</b> 

---

### Run details
| | |
| --- | --- |
| **Project** | Temporal UI |
| **Status** | Passed |
| **Commit** | 8eb973e417 <span style="cursor:help;" title="Your CI provider generated this commit by merging this branch into the target branch.">ℹ️</span> |
| **Started** | Apr 6, 2022 6:37 AM |
| **Ended** | Apr 6, 2022 6:38 AM |
| **Duration** | 01:10 <a href="https://on.cypress.io/parallelization?utm_source=github" title="Enable parallelization to reduce run duration">💡</a> |
| **OS** | Linux Ubuntu - 20.04 |
| **Browser** | Chrome 100 <img src="https://cdnjs.cloudflare.com/ajax/libs/browser-logos/48.0.4/chrome/chrome_32x32.png" width="16" height="16" /> |

[View run in Cypress Dashboard ➡️](https://dashboard.cypress.io/projects/g7xfxw/runs/69/overview?utm_source=github)





---

This comment has been generated by **cypress-bot** as a result of this project's GitHub integration settings. You can manage this integration in [this project's settings in the Cypress Dashboard](https://dashboard.cypress.io/projects/g7xfxw/settings?utm_source=github)



Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b53c02fa..c5490baf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,6 @@
   "jest.nodeEnv": {
     "TZ": "UTC"
   },
-  "jest.jestCommandLine": "npm test --"
+  "jest.jestCommandLine": "npm test --",
+  "editor.formatOnSave": true
 }
diff --git a/src/global.d.ts b/src/global.d.ts
index de0fe4bd..f62577ea 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -54,6 +54,7 @@ type Settings = {
   };
   baseUrl: string;
   defaultNamespace: string;
+  showTemporalSystemNamespace: boolean;
   runtimeEnvironment: {
     isCloud: boolean;
     isLocal: boolean;
diff --git a/src/lib/components/dropdown-menu.svelte b/src/lib/components/dropdown-menu.svelte
index 640ee84e..5f3e5e66 100644
--- a/src/lib/components/dropdown-menu.svelte
+++ b/src/lib/components/dropdown-menu.svelte
@@ -18,7 +18,7 @@
 
   let show: boolean = false;
   let menu: any = null;
-  $: _value = $page.url?.searchParams?.get(parameter) ?? undefined;
+  $: _value = $page.url?.searchParams?.get(parameter);
 
   $: {
     updateQueryParameters({
diff --git a/src/lib/components/event/event-details.svelte b/src/lib/components/event/event-details.svelte
index 1ee0437d..1b6ca4a7 100644
--- a/src/lib/components/event/event-details.svelte
+++ b/src/lib/components/event/event-details.svelte
@@ -1,8 +1,16 @@
 <script lang="ts">
+  import { page } from '$app/stores';
   import { format } from '$lib/utilities/format-camel-case';
+  import { routeForWorkflow } from '$lib/utilities/route-for';
   import CodeBlock from '$lib/components/code-block.svelte';
-  import { shouldDisplayAttribute } from '$lib/utilities/get-single-attribute-for-event';
+  import TableLink from '$lib/components/table-link.svelte';
+  import {
+    shouldDisplayAttribute,
+    shouldDisplayAsWorkflowLink,
+  } from '$lib/utilities/get-single-attribute-for-event';
   export let event: HistoryEventWithId;
+
+  const { workflow, namespace } = $page.params;
 </script>
 
 <section>
@@ -15,6 +23,11 @@
         <div class="flex-grow w-full">
           {#if typeof value === 'object'}
             <CodeBlock content={value} />
+          {:else if shouldDisplayAsWorkflowLink(key)}
+            <TableLink
+              href={routeForWorkflow({ namespace, workflow, run: value })}
+              >{value}</TableLink
+            >
           {:else}
             <p><span class="bg-gray-300 text-gray-700 px-2">{value}</span></p>
           {/if}
diff --git a/src/lib/components/event/event-single-detail.svelte b/src/lib/components/event/event-single-detail.svelte
index 7b8f9554..98130a27 100644
--- a/src/lib/components/event/event-single-detail.svelte
+++ b/src/lib/components/event/event-single-detail.svelte
@@ -1,11 +1,18 @@
 <script lang="ts">
-  import { getSingleAttributeForEvent } from '$lib/utilities/get-single-attribute-for-event';
+  import { page } from '$app/stores';
+  import { routeForWorkflow } from '$lib/utilities/route-for';
+  import {
+    getSingleAttributeForEvent,
+    shouldDisplayAsWorkflowLink,
+  } from '$lib/utilities/get-single-attribute-for-event';
   import CodeBlock from '../code-block.svelte';
+  import TableLink from '$lib/components/table-link.svelte';
 
   export let event: HistoryEventWithId | null = null;
   export let eventGroup: CompactEventGroup | null = null;
 
   let { key, value } = getSingleAttributeForEvent({ event, eventGroup });
+  const { workflow, namespace } = $page.params;
 </script>
 
 <section>
@@ -16,6 +23,10 @@
     <div class="flex-grow w-full">
       {#if eventGroup}
         <CodeBlock content={value} />
+      {:else if shouldDisplayAsWorkflowLink(key)}
+        <TableLink href={routeForWorkflow({ namespace, workflow, run: value })}
+          >{value}</TableLink
+        >
       {:else}
         <p><span class="bg-gray-300 text-gray-700 px-2">{value}</span></p>
       {/if}
diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte
index 3f257fb0..4170c729 100644
--- a/src/lib/components/event/event-summary-row.svelte
+++ b/src/lib/components/event/event-summary-row.svelte
@@ -5,23 +5,23 @@
   import EventSingleDetail from './event-single-detail.svelte';
 
   export let event: HistoryEventWithId;
-  export let expanded: boolean = false;
+  export let expandAll: boolean = false;
 
-  $: collapsed = expanded ? false : true;
+  $: expanded = expandAll;
 
   const onLinkClick = () => {
-    if (!expanded) {
-      collapsed = !collapsed;
+    if (!expandAll) {
+      expanded = !expanded;
     }
   };
 </script>
 
 <article class="row" id={event.id}>
   <div class="cell">
-    <span class="text-gray-500 text-normal mx-1">{event.id}</span>
+    <a class="text-gray-500 text-normal mx-1" href="#{event.id}">{event.id}</a>
     <span
       class="mx-2"
-      class:link={!expanded}
+      class:link={!expandAll}
       on:click|stopPropagation={onLinkClick}>{event.name}</span
     >
   </div>
@@ -29,10 +29,10 @@
     {formatDate(event?.eventTime)}
   </div>
   <div class="cell links">
-    {#if collapsed}
-      <EventSingleDetail {event} />
-    {:else}
+    {#if expanded}
       <EventDetails {event} />
+    {:else}
+      <EventSingleDetail {event} />
     {/if}
   </div>
 </article>
diff --git a/src/lib/components/event/view/event-full.svelte b/src/lib/components/event/view/event-full.svelte
index c8e55496..af0b360d 100644
--- a/src/lib/components/event/view/event-full.svelte
+++ b/src/lib/components/event/view/event-full.svelte
@@ -15,7 +15,7 @@
   <Pagination {items} floatId="event-view-toggle" let:visibleItems>
     <EventSummaryTable>
       {#each visibleItems as event (event.id)}
-        <EventSummaryRow {event} expanded />
+        <EventSummaryRow {event} expandAll />
       {/each}
       {#if !items?.length}
         <EventEmptyRow />
diff --git a/src/lib/components/pagination.svelte b/src/lib/components/pagination.svelte
index 698c9bfe..90a51717 100644
--- a/src/lib/components/pagination.svelte
+++ b/src/lib/components/pagination.svelte
@@ -13,6 +13,7 @@
   export let floatId: string | undefined = undefined;
 
   import FilterSelect from './select/filter-select.svelte';
+  import { getFloatStyle } from '$lib/utilities/get-float-style';
 
   export let startingIndex: string | number = 0;
 
@@ -31,11 +32,7 @@
     }
   });
 
-  // If float width and nav height exist and screen is above lg breakpoint, float the nav
-  $: floatStyle =
-    width && height && screenWidth > 1024
-      ? `position: absolute; right: ${width + 20}px; top: -${height + 14}px`
-      : '';
+  $: floatStyle = getFloatStyle({ width, height, screenWidth });
 </script>
 
 <svelte:window bind:innerWidth={screenWidth} />
diff --git a/src/lib/services/settings-service.ts b/src/lib/services/settings-service.ts
index d6d04ef6..b662f830 100644
--- a/src/lib/services/settings-service.ts
+++ b/src/lib/services/settings-service.ts
@@ -29,6 +29,7 @@ export const fetchSettings = async (
     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
+    showTemporalSystemNamespace: settings?.ShowTemporalSystemNamespace,
     runtimeEnvironment: {
       get isCloud() {
         if (EnvironmentOverride) {
diff --git a/src/lib/utilities/get-float-style.ts b/src/lib/utilities/get-float-style.ts
new file mode 100644
index 00000000..3d2b828a
--- /dev/null
+++ b/src/lib/utilities/get-float-style.ts
@@ -0,0 +1,15 @@
+export const getFloatStyle = ({
+  width,
+  height,
+  screenWidth,
+  breakpoint = 1024,
+}: {
+  width?: number;
+  height?: number;
+  screenWidth?: number;
+  breakpoint?: number;
+}): string => {
+  return width && height && screenWidth > breakpoint
+    ? `position: absolute; right: ${width + 20}px; top: -${height + 14}px`
+    : '';
+};
diff --git a/src/lib/utilities/get-single-attribute-for-event.ts b/src/lib/utilities/get-single-attribute-for-event.ts
index 60ddfa26..c6a975fc 100644
--- a/src/lib/utilities/get-single-attribute-for-event.ts
+++ b/src/lib/utilities/get-single-attribute-for-event.ts
@@ -10,6 +10,16 @@ export const shouldDisplayAttribute = (
   return true;
 };
 
+export const shouldDisplayAsWorkflowLink = (key: string) => {
+  if (!key) return false;
+
+  if (key.toLowerCase().endsWith('runid')) {
+    return true;
+  }
+
+  return false;
+};
+
 const mapObjectToPropertValue = (key: string, value: any) => {
   const firstKey = Object.keys(value)[0];
   return { key: `${key}.${firstKey}`, value: value[firstKey] };
diff --git a/src/routes/_namespace-select.svelte b/src/routes/_namespace-select.svelte
index 89bb787a..d98044f4 100644
--- a/src/routes/_namespace-select.svelte
+++ b/src/routes/_namespace-select.svelte
@@ -6,6 +6,7 @@
 
   import { routeForWorkflows } from '$lib/utilities/route-for';
 
+  const { showTemporalSystemNamespace } = $page.stuff.settings;
   const { isCloud } = $page.stuff.settings.runtimeEnvironment;
 
   import Select from '$lib/components/select/select.svelte';
@@ -16,7 +17,10 @@
 
   let namespaces = ($page.stuff.namespaces || [])
     .map((namespace: Namespace) => namespace?.namespaceInfo?.name)
-    .filter((namespace: string) => namespace !== 'temporal-system');
+    .filter(
+      (namespace: string) =>
+        showTemporalSystemNamespace || namespace !== 'temporal-system',
+    );
 
   const switchNamespaces = (event: Event) => {
     const target = event.target as HTMLSelectElement;
diff --git a/src/types/index.ts b/src/types/index.ts
index b010d3d0..ec4f236a 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -130,4 +130,5 @@ export type Timestamp = google.protobuf.ITimestamp;
 export type SettingsResponse = {
   Auth: { Enabled: boolean; Options: string[] };
   DefaultNamespace: string;
+  ShowTemporalSystemNamespace: boolean;
 };


Test patch:
diff --git a/src/lib/utilities/get-float-style.test.ts b/src/lib/utilities/get-float-style.test.ts
new file mode 100644
index 00000000..cdfcb7bd
--- /dev/null
+++ b/src/lib/utilities/get-float-style.test.ts
@@ -0,0 +1,69 @@
+import { getFloatStyle } from './get-float-style';
+
+describe(getFloatStyle, () => {
+  it('should return a style string for a width/height and screenWidth over default breakpoint', () => {
+    const dimensions = {
+      width: 390,
+      height: 82,
+      screenWidth: 1400,
+    };
+    const style = 'position: absolute; right: 410px; top: -96px';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return a style string for a width/height and screenWidth over custom breakpoint', () => {
+    const dimensions = {
+      width: 390,
+      height: 82,
+      screenWidth: 1400,
+      breakpoint: 900,
+    };
+    const style = 'position: absolute; right: 410px; top: -96px';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return an empty string for a width/height and screenWidth under default breakpoint', () => {
+    const dimensions = {
+      width: 390,
+      height: 82,
+      screenWidth: 600,
+    };
+    const style = '';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return an empty string for a width/height and screenWidth under default breakpoint', () => {
+    const dimensions = {
+      width: 390,
+      height: 82,
+      screenWidth: 750,
+      breakpoint: 800,
+    };
+    const style = '';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return an empty string if missing width', () => {
+    const dimensions = {
+      height: 82,
+      screenWidth: 750,
+      breakpoint: 800,
+    };
+    const style = '';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return an empty string if missing height', () => {
+    const dimensions = {
+      width: 390,
+      screenWidth: 750,
+      breakpoint: 800,
+    };
+    const style = '';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+  it('should return an empty string if missing screenWidth', () => {
+    const dimensions = {
+      height: 82,
+      width: 390,
+      breakpoint: 800,
+    };
+    const style = '';
+    expect(getFloatStyle(dimensions)).toEqual(style);
+  });
+});
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 105a2087..61c68783 100644
--- a/src/lib/utilities/get-single-attribute-for-event.test.ts
+++ b/src/lib/utilities/get-single-attribute-for-event.test.ts
@@ -1,4 +1,7 @@
-import { getSingleAttributeForEvent } from './get-single-attribute-for-event';
+import {
+  getSingleAttributeForEvent,
+  shouldDisplayAsWorkflowLink,
+} from './get-single-attribute-for-event';
 
 describe(getSingleAttributeForEvent, () => {
   const workflowEvent = {
@@ -157,3 +160,23 @@ describe(getSingleAttributeForEvent, () => {
     ).toStrictEqual({ key: 'input', value });
   });
 });
+
+describe(shouldDisplayAsWorkflowLink, () => {
+  it('should return true for event keys that end with RunId', () => {
+    expect(shouldDisplayAsWorkflowLink('originalExecutionRunId')).toBe(true);
+    expect(shouldDisplayAsWorkflowLink('firstExecutionRunId')).toBe(true);
+    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);
+    expect(shouldDisplayAsWorkflowLink('parentInitiatedEventId')).toBe(false);
+    expect(shouldDisplayAsWorkflowLink('inlineRunIdSample')).toBe(false);
+  });
+});