Repository: temporalio/ui
Pull request #229 (2022-01-13 22:16:28)

Transform payloads
<!--- 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 -->

## Why?
<!-- Tell your future self why have you made these changes -->

## 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 -->

3. Any docs updates needed?
<!--- update README if applicable
      or point out where to update docs.temporal.io -->



Patch:
diff --git a/.github/workflows/trigger-ui-server-update.yml b/.github/workflows/trigger-ui-server-update.yml
index 4db3a6a3..3a79ed52 100644
--- a/.github/workflows/trigger-ui-server-update.yml
+++ b/.github/workflows/trigger-ui-server-update.yml
@@ -18,9 +18,9 @@ jobs:
     steps:
       - name: Dispatch ui-server Github Action
         env:
-          PAT: ${{ secrets.PAT }}
+          PAT: ${{ secrets.COMMANDER_DATA_TOKEN }}
           PARENT_REPO: temporalio/ui-server
           PARENT_BRANCH: main
           WORKFLOW_ID: update-ui.yml
         run: |
-          curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ env.COMMANDER_DATA_TOKEN }}" https://api.github.com/repos/${{ env.PARENT_REPO }}/actions/workflows/${{ env.WORKFLOW_ID }}/dispatches -d '{"ref":"${{ env.PARENT_BRANCH }}"}'
+          curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ env.PAT }}" https://api.github.com/repos/${{ env.PARENT_REPO }}/actions/workflows/${{ env.WORKFLOW_ID }}/dispatches -d '{"ref":"${{ env.PARENT_BRANCH }}"}'
diff --git a/src/lib/stores/banner.ts b/src/lib/stores/banner.ts
index f4b4a8e7..02a3218e 100644
--- a/src/lib/stores/banner.ts
+++ b/src/lib/stores/banner.ts
@@ -1,4 +1,4 @@
-import { persistStore } from '$lib/utilities/persist-store';
+import { persistStore } from '$lib/stores/persist-store';
 
 export const closedBannerId = persistStore('closedBannerId', null);
 
diff --git a/src/lib/stores/data-converter-config.ts b/src/lib/stores/data-converter-config.ts
index d7bb91cb..f509bbfd 100644
--- a/src/lib/stores/data-converter-config.ts
+++ b/src/lib/stores/data-converter-config.ts
@@ -1,5 +1,5 @@
 import { writable } from 'svelte/store';
-import { persistStore } from '$lib/utilities/persist-store';
+import { persistStore } from '$lib/stores/persist-store';
 
 export const dataConverterPort = persistStore('port', null);
 
diff --git a/src/lib/utilities/persist-store.ts b/src/lib/stores/persist-store.ts
similarity index 100%
rename from src/lib/utilities/persist-store.ts
rename to src/lib/stores/persist-store.ts
diff --git a/src/lib/stores/data-converter-websocket.ts b/src/lib/utilities/data-converter-websocket.ts
similarity index 95%
rename from src/lib/stores/data-converter-websocket.ts
rename to src/lib/utilities/data-converter-websocket.ts
index 498dbc38..b1b5ba56 100644
--- a/src/lib/stores/data-converter-websocket.ts
+++ b/src/lib/utilities/data-converter-websocket.ts
@@ -3,7 +3,7 @@ import WebSocketAsPromised from 'websocket-as-promised';
 import {
   dataConverterPort,
   setLastDataConverterFailure,
-} from './data-converter-config';
+} from '../stores/data-converter-config';
 
 let sock = null;
 const port = get(dataConverterPort) ?? null;
diff --git a/src/lib/utilities/decode-payload-lazy.ts b/src/lib/utilities/decode-payload-lazy.ts
new file mode 100644
index 00000000..8938fdcd
--- /dev/null
+++ b/src/lib/utilities/decode-payload-lazy.ts
@@ -0,0 +1,23 @@
+import type { Payload } from '$types';
+
+export function decodePayload(
+  payload: Payload,
+  // This could decode to any object. So we either use the payload object passed in or decode it
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Payload | Record<any, any> | string {
+  const encoding = window.atob(String(payload.metadata.encoding));
+  // Help users out with an english encoding
+  (payload.metadata.encodingDecrypted as unknown as string) = encoding;
+
+  switch (encoding) {
+    case 'json/plain':
+    case 'json/protobuf':
+      try {
+        return JSON.parse(window.atob(String(payload.data)));
+      } catch (_e) {
+        // Couldn't correctly decode this just let the user deal with the data as is
+      }
+  }
+
+  return payload;
+}
diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts
index 3445820d..d46b3dac 100644
--- a/src/lib/utilities/decode-payload.ts
+++ b/src/lib/utilities/decode-payload.ts
@@ -1,8 +1,9 @@
 import type { Payload } from '$types';
 import { dataConverterPort } from '$lib/stores/data-converter-config';
-import { dataConverterWebsocket } from '$lib/stores/data-converter-websocket';
+import { dataConverterWebsocket } from '$lib/utilities/data-converter-websocket';
 import { get } from 'svelte/store';
 import { convertPayload } from '$lib/services/data-converter';
+import { decodePayload } from './decode-payload-lazy';
 
 export const convertPayloadToJson = async (
   eventAttribute: EventAttribute,
@@ -26,13 +27,7 @@ export const convertPayloadToJson = async (
     return Promise.resolve(eventAttribute);
   }
 
-  // This is a very sneaky array[0].data It's a holdover from a previous version of this code we could probably actually iterate
-  // over this and get each entry decoded from base64
-  // This was set to any because we just want whatever comes out of here to be base64 decoded and reset to the other value
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  let [JSONPayload]: any[] = potentialPayload.map((payload) => payload.data);
-
-  JSONPayload = window.atob(JSONPayload);
+  let JSONPayload = potentialPayload.map(decodePayload);
 
   if (port) {
     const webSocket = dataConverterWebsocket.websocket;
@@ -52,5 +47,42 @@ export const convertPayloadToJson = async (
     anyAttributes.result.payloads = JSONPayload;
   }
 
+  // Decode Search Attributes
+  if (anyAttributes?.searchAttributes?.indexedFields) {
+    const searchAttributes = anyAttributes?.searchAttributes?.indexedFields;
+
+    Object.entries(searchAttributes).forEach(([key, value]) => {
+      searchAttributes[key] = decodePayload(value);
+    });
+  }
+
+  // Decode Memo
+  if (anyAttributes?.memo?.fields) {
+    const memo = anyAttributes?.memo?.fields;
+
+    Object.entries(memo).forEach(([key, value]) => {
+      memo[key] = decodePayload(value);
+    });
+  }
+
+  // Decode Header
+  if (anyAttributes?.header?.fields) {
+    const header = anyAttributes?.header?.fields;
+
+    Object.entries(header).forEach(([key, value]) => {
+      header[key] = decodePayload(value);
+    });
+  }
+
+  // Decode Query Result
+  // This one is a best guess from the previous codebase and needs verified
+  if (anyAttributes?.queryResult) {
+    const queryResult = anyAttributes?.queryResult;
+
+    Object.entries(queryResult).forEach(([key, value]) => {
+      queryResult[key] = decodePayload(value);
+    });
+  }
+
   return anyAttributes;
 };
diff --git a/src/routes/_header.svelte b/src/routes/_header.svelte
index 6fb9777e..f267fc51 100644
--- a/src/routes/_header.svelte
+++ b/src/routes/_header.svelte
@@ -36,18 +36,25 @@
     </a>
     {#if $settings.auth?.enabled}
       {#if $user?.email}
-        <img
-          src={$user.picture}
-          alt="User Avatar"
-          class="rounded-full h-6 w-6"
-        />
+        <button
+          class="header-button min-w-min"
+          on:click={() =>
+            window.location.assign(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={() =>
             window.location.assign(import.meta.env.VITE_API + '/auth/sso')}
         >
-          Sign In
+          Login
         </button>
       {/if}
     {/if}
@@ -56,7 +63,7 @@
 
 <style lang="postcss">
   .header-button {
-    @apply block px-4 py-2 text-sm text-white;
+    @apply block px-4 py-2 text-sm text-white flex items-center justify-center;
   }
 
   .header-button:hover {


Test patch:
diff --git a/src/lib/utilities/decode-payload.test.ts b/src/lib/utilities/decode-payload.test.ts
new file mode 100644
index 00000000..df814c37
--- /dev/null
+++ b/src/lib/utilities/decode-payload.test.ts
@@ -0,0 +1,59 @@
+/**
+ * @jest-environment jsdom
+ */
+
+// Use jsdom jest environment for access to window.atob
+
+import { decodePayload } from './decode-payload-lazy';
+const WebDecodePayload = {
+  metadata: {
+    encoding: 'YmluYXJ5L2VuY3J5cHRlZA==',
+    'encryption-key-id': '',
+  },
+  data: 'dlSjfJltMHoITwMRv4gqQZsf3yXLo5UVtroA7ZXM3Eeggnrkzc+h6+xhQm2TQ6z8rska0QtWu7Ye3AhYfGw+8mY6/5NN+La4TJmKOe5/EKfL2znMbIXBzXLaeK4MjjIrxo2gI1weYHBb',
+};
+
+const JsonPlainEncoded = {
+  metadata: {
+    encoding: 'anNvbi9wbGFpbg==',
+    type: 'S2V5d29yZA==',
+  },
+  data: 'InRlc3RAdGVzdC5jb20i',
+};
+
+const ProtobufEncoded = {
+  metadata: {
+    encoding: 'anNvbi9wcm90b2J1Zg==',
+    type: 'S2V5d29yZA==',
+  },
+  data: 'InRlc3RAdGVzdC5jb20i',
+};
+
+const BinaryNullEncodedNoData = {
+  metadata: {
+    encoding: 'YmluYXJ5L251bGw=',
+  },
+  data: null,
+};
+
+const Base64Decoded = 'test@test.com';
+
+describe(decodePayload, () => {
+  it('Should not decode a payload with encoding binary/encrypted', () => {
+    expect(decodePayload(WebDecodePayload)).toEqual(WebDecodePayload);
+  });
+
+  it('Should not decode a payload with encoding binary/encrypted', () => {
+    expect(decodePayload(BinaryNullEncodedNoData)).toEqual(
+      BinaryNullEncodedNoData,
+    );
+  });
+
+  it('Should decode a payload with encoding json/plain', () => {
+    expect(decodePayload(JsonPlainEncoded)).toEqual(Base64Decoded);
+  });
+
+  it('Should decode a payload with encoding json/protobuf', () => {
+    expect(decodePayload(ProtobufEncoded)).toEqual(Base64Decoded);
+  });
+});