Repository: temporalio/ui
Pull request #106 (2021-10-11 23:42:31)

Fix build issues
The build was failing on Lodash. But, we have optional chaining now, which is the main reason we pulled in the dependency and @feedmeapples has been looking to get rid of it. So, two birds with one stone. This PR also addresses some other lingering issues after the upgrade of SvelteKit.


Patch:
diff --git a/package-lock.json b/package-lock.json
index 2607fa23..c55425c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,11 +8,9 @@
       "name": "temporal-ui",
       "version": "0.0.1",
       "dependencies": {
-        "@types/lodash": "^4.14.173",
         "base-64": "^1.0.0",
         "date-fns": "^2.23.0",
         "json-beautify": "^1.1.1",
-        "lodash": "^4.17.21",
         "svelte-hero-icons": "^3.0.0",
         "url-pattern": "^1.0.3"
       },
@@ -1450,11 +1448,6 @@
       "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
       "dev": true
     },
-    "node_modules/@types/lodash": {
-      "version": "4.14.175",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
-      "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw=="
-    },
     "node_modules/@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -6067,7 +6060,8 @@
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "node_modules/lodash.clonedeep": {
       "version": "4.5.0",
@@ -10275,11 +10269,6 @@
       "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
       "dev": true
     },
-    "@types/lodash": {
-      "version": "4.14.175",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
-      "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw=="
-    },
     "@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -13719,7 +13708,8 @@
     "lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "lodash.clonedeep": {
       "version": "4.5.0",
diff --git a/package.json b/package.json
index 61fd0866..85a665fa 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,6 @@
     "base-64": "^1.0.0",
     "date-fns": "^2.23.0",
     "json-beautify": "^1.1.1",
-    "lodash": "^4.17.21",
     "svelte-hero-icons": "^3.0.0",
     "url-pattern": "^1.0.3"
   },
@@ -36,7 +35,6 @@
     "@temporalio/proto": "^0.3.1",
     "@types/base-64": "^1.0.0",
     "@types/jest": "^27.0.1",
-    "@types/lodash": "^4.14.173",
     "@typescript-eslint/eslint-plugin": "^4.30.0",
     "@typescript-eslint/parser": "^4.30.0",
     "autoprefixer": "^10.3.3",
diff --git a/src/lib/models/workflow-execution.ts b/src/lib/models/workflow-execution.ts
index 8d7dceae..fb87c042 100644
--- a/src/lib/models/workflow-execution.ts
+++ b/src/lib/models/workflow-execution.ts
@@ -6,7 +6,6 @@ import type {
 } from '$types';
 
 import { formatDate } from '$lib/utilities/format-date';
-import get from 'lodash/get';
 
 type Optional<T extends unknown, K extends keyof T = keyof T> = Omit<T, K> &
   Partial<Pick<T, K>>;
@@ -41,7 +40,7 @@ export const toWorkflowExecution = (
   const status = response.workflowExecutionInfo.status;
   const historyEvents = response.workflowExecutionInfo.historyLength;
   const url = `/workflows/${id}/${runId}`;
-  const taskQueue = get(response, 'executionConfig.taskQueue.name');
+  const taskQueue = response?.executionConfig?.taskQueue?.name;
   const pendingActivities = response.pendingActivities || [];
 
   const toggleUrl = (isFullScreen: boolean): string => {
diff --git a/src/lib/services/workflow-execution-service.ts b/src/lib/services/workflow-execution-service.ts
index 7e4f377b..ef0d6b65 100644
--- a/src/lib/services/workflow-execution-service.ts
+++ b/src/lib/services/workflow-execution-service.ts
@@ -1,6 +1,4 @@
 import { sub, formatISO } from 'date-fns';
-import isFunction from 'lodash/isFunction';
-import noop from 'lodash/noop';
 
 import type {
   DescribeWorkflowExecutionResponse,
@@ -10,8 +8,8 @@ import type {
 
 import { paginated } from '$lib/utilities/paginated';
 import { requestFromAPI } from '$lib/utilities/request-from-api';
+import { isFunction } from '$lib/utilities/is-function';
 
-const id = <T>(x: T) => x;
 const createDate = (d: Duration) => formatISO(sub(new Date(), d));
 
 export type GetWorkflowExecutionRequest = NamespaceScopedRequest & {
@@ -40,9 +38,9 @@ const fetchWorkflows =
   async (
     {
       namespace,
-      onStart = noop,
-      onUpdate = id,
-      onComplete = id,
+      onStart,
+      onUpdate,
+      onComplete,
       startTime = { days: 1 },
     }: FetchWorkflows<ListWorkflowExecutionsResponse>,
     request = fetch,
@@ -122,14 +120,7 @@ export async function fetchWorkflow(
 }
 
 export const fetchEvents = async (
-  {
-    namespace,
-    executionId,
-    runId,
-    onStart = noop,
-    onUpdate = id,
-    onComplete = id,
-  }: FetchEvents,
+  { namespace, executionId, runId, onStart, onUpdate, onComplete }: FetchEvents,
   request = fetch,
 ): Promise<GetWorkflowExecutionHistoryResponse> => {
   const events: GetWorkflowExecutionHistoryResponse = await paginated(
diff --git a/src/lib/stores/activities.ts b/src/lib/stores/activities.ts
index 33e4e2cd..71e12e94 100644
--- a/src/lib/stores/activities.ts
+++ b/src/lib/stores/activities.ts
@@ -1,4 +1,6 @@
-import { derived, Readable } from 'svelte/store';
+import type { Readable } from 'svelte/store';
+import { derived } from 'svelte/store';
+
 import {
   isActivityTaskScheduledEvent,
   isActivityTaskStartedEvent,
diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts
index f82c3dda..b016b253 100644
--- a/src/lib/stores/events.ts
+++ b/src/lib/stores/events.ts
@@ -1,13 +1,14 @@
 import { derived, writable } from 'svelte/store';
 
-import get from 'lodash/get';
-import set from 'lodash/set';
+import type { HistoryEventWithId } from '$lib/models/event-history';
+import type { GetWorkflowExecutionHistoryResponse } from '$types';
 
 import { fetchEvents } from '$lib/services/workflow-execution-service';
 import { createQueryStore } from '$lib/utilities/create-query-store';
+import { toEventHistory } from '$lib/models/event-history';
+import { set } from '$lib/utilities/set-object-key';
+
 import { createActivityStore } from './activities';
-import { HistoryEventWithId, toEventHistory } from '$lib/models/event-history';
-import type { GetWorkflowExecutionHistoryResponse } from '$types';
 
 const stores: { [key: string]: ReturnType<typeof createStore> } = {};
 
@@ -58,12 +59,14 @@ export const createEventStore = (
   namespace: string,
   executionId: string,
   runId: string,
-) => {
+): ReturnType<typeof createStore> => {
+  let store = stores?.[namespace]?.[executionId]?.[runId];
   const path = `${namespace}.${executionId}.${runId}`;
-  if (!get(stores, path)) {
-    console.log('no store found');
-    set(stores, path, createStore(namespace, executionId, runId));
+
+  if (!store) {
+    store = createStore(namespace, executionId, runId);
+    set(stores, path, store);
   }
 
-  return get(stores, path);
+  return store;
 };
diff --git a/src/lib/stores/workflows.ts b/src/lib/stores/workflows.ts
index fea1dfeb..85676059 100644
--- a/src/lib/stores/workflows.ts
+++ b/src/lib/stores/workflows.ts
@@ -74,7 +74,9 @@ export const createStore = (namespace: string) => {
   };
 };
 
-export const createWorkflowStore = (namespace: string) => {
+export const createWorkflowStore = (
+  namespace: string,
+): ReturnType<typeof createStore> => {
   if (!stores[namespace]) stores[namespace] = createStore(namespace);
   return stores[namespace];
 };
diff --git a/src/lib/utilities/create-query-store.ts b/src/lib/utilities/create-query-store.ts
index 536782bb..cb8c54d1 100644
--- a/src/lib/utilities/create-query-store.ts
+++ b/src/lib/utilities/create-query-store.ts
@@ -43,14 +43,14 @@ const collectData = <T extends HasId>(
 export const createQueryStore = <
   FormattedType extends HasId,
   ResponseType,
-  FetchType extends (options: any) => unknown,
+  FetchType extends (options: Record<string, unknown>) => unknown,
   Options = Parameters<FetchType>[0],
 >(
   fetch: FetchType,
   format: Formatter<ResponseType, FormattedType>,
   options: Options,
   dependencies: Readable<Partial<Options>>[] = [],
-) => {
+): Writable<QueryStore<FormattedType>> => {
   let parameters = options;
 
   const request = () => {
diff --git a/src/lib/utilities/create-store-with-callback.ts b/src/lib/utilities/create-store-with-callback.ts
index d3faf4bd..ea545fef 100644
--- a/src/lib/utilities/create-store-with-callback.ts
+++ b/src/lib/utilities/create-store-with-callback.ts
@@ -1,5 +1,5 @@
 import { Updater, Writable, writable } from 'svelte/store';
-import isFunction from 'lodash/isFunction';
+import { isFunction } from './is-function';
 
 const call = (fn: () => void) => {
   if (isFunction(fn)) fn();
diff --git a/src/lib/utilities/get-activity-url.ts b/src/lib/utilities/get-activity-url.ts
index 595a6c65..ae258fb5 100644
--- a/src/lib/utilities/get-activity-url.ts
+++ b/src/lib/utilities/get-activity-url.ts
@@ -1,5 +1,5 @@
 export const getActivityUrl =
   (namespace: string, executionId: string, runId: string) =>
-  (activityId = '') => {
+  (activityId = ''): string => {
     return `/namespaces/${namespace}/workflows/${executionId}/${runId}/activities/${activityId}`;
   };
diff --git a/src/lib/utilities/is-function.ts b/src/lib/utilities/is-function.ts
new file mode 100644
index 00000000..878a6c51
--- /dev/null
+++ b/src/lib/utilities/is-function.ts
@@ -0,0 +1,4 @@
+// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
+export const isFunction = (fn: any): boolean => {
+  return typeof fn === 'function';
+};
diff --git a/src/lib/utilities/omit.ts b/src/lib/utilities/omit.ts
new file mode 100644
index 00000000..7bb97441
--- /dev/null
+++ b/src/lib/utilities/omit.ts
@@ -0,0 +1,26 @@
+type Omit = {
+  // eslint-disable-next-line @typescript-eslint/ban-types
+  <Source extends object, KeysToOmit extends string[]>(
+    obj: Source,
+    ...keys: KeysToOmit
+  ): {
+    [RemainingKey in Exclude<
+      keyof Source,
+      KeysToOmit[number]
+    >]: Source[RemainingKey];
+  };
+};
+
+export const omit: Omit = (object, ...keys) => {
+  const result = {} as {
+    [K in keyof typeof object]: typeof object[K];
+  };
+
+  for (const key of Object.keys(object)) {
+    if (!keys.includes(key)) {
+      result[key] = object[key];
+    }
+  }
+
+  return result;
+};
diff --git a/src/lib/utilities/paginated.ts b/src/lib/utilities/paginated.ts
index f481ba45..26b19980 100644
--- a/src/lib/utilities/paginated.ts
+++ b/src/lib/utilities/paginated.ts
@@ -1,4 +1,4 @@
-import isFunction from 'lodash/isFunction';
+import { isFunction } from './is-function';
 import { merge } from './merge';
 
 type PaginatedOptions<T> = PaginationCallbacks<T> & {
@@ -17,6 +17,7 @@ type PaginatedOptions<T> = PaginationCallbacks<T> & {
  * - `onComplete` fires when there are no more `nextPageTokens`.
  * - `onError` fires when a promise is rejected.
  */
+
 export const paginated = async <T extends WithNextPageToken>(
   fn: (token?: NextPageToken) => Promise<T>,
   {
diff --git a/src/lib/utilities/set-object-key.ts b/src/lib/utilities/set-object-key.ts
new file mode 100644
index 00000000..e5441cba
--- /dev/null
+++ b/src/lib/utilities/set-object-key.ts
@@ -0,0 +1,22 @@
+const isLast = (path: string[]) => path.length === 0;
+
+export const set = <T = Record<string, unknown>>(
+  object: T,
+  path: string | string[],
+  value: unknown,
+): T => {
+  let current = object;
+  if (!Array.isArray(path)) path = path.split('.');
+
+  while (path.length) {
+    const segment = path.shift();
+    if (isLast(path)) {
+      current[segment] = value;
+    } else {
+      if (typeof current[segment] === 'undefined') current[segment] = {};
+      current = current[segment];
+    }
+  }
+
+  return object;
+};
diff --git a/src/lib/utilities/to-duration.ts b/src/lib/utilities/to-duration.ts
index d540fa96..518d10d0 100644
--- a/src/lib/utilities/to-duration.ts
+++ b/src/lib/utilities/to-duration.ts
@@ -6,7 +6,7 @@ export const toDuration = (value: string): Duration => {
   return { [units]: parseInt(amount, 10) };
 };
 
-export const toString = (duration: Duration) => {
+export const toString = (duration: Duration): string => {
   const units = Object.keys(duration)[0];
   const amount: number = duration[units];
   return `${amount} ${units}`;
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index e0ef7566..55e5ef95 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -1,14 +1,16 @@
 <script context="module" lang="ts">
   import type { LoadInput } from '@sveltejs/kit';
+  import { requestFromAPI } from '$lib/utilities/request-from-api';
   import type {
     DescribeNamespaceResponse,
     ListNamespacesResponse,
   } from '$types';
 
   export async function load({ fetch }: LoadInput) {
-    const { namespaces }: ListNamespacesResponse = await fetch(
-      import.meta.env.VITE_API + '/namespaces',
-    ).then((response) => response.json());
+    const { namespaces }: ListNamespacesResponse = await requestFromAPI(
+      '/namespaces',
+      { request: fetch },
+    );
 
     return {
       props: { namespaces },
@@ -32,7 +34,7 @@
   <title>Temporal</title>
 
   <link rel="manifest" href="/site.webmanifest" />
-  <link rel="apple-touch-icon" href="apple-touch-icon.png" />
+  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
 
   <meta property="og:title" content="Temporal" />
   <meta property="og:type" content="website" />
diff --git a/src/routes/namespaces/[namespace]/settings.svelte b/src/routes/namespaces/[namespace]/settings.svelte
index c2d06b84..efb59daa 100644
--- a/src/routes/namespaces/[namespace]/settings.svelte
+++ b/src/routes/namespaces/[namespace]/settings.svelte
@@ -1,13 +1,15 @@
 <script context="module" lang="ts">
   import type { LoadInput } from '@sveltejs/kit';
   import type { DescribeNamespaceResponse } from '$types';
+  import { requestFromAPI } from '$lib/utilities/request-from-api';
 
   export async function load({ fetch, page }: LoadInput) {
     const { namespace } = page.params;
 
-    const settings: DescribeNamespaceResponse = await fetch(
-      import.meta.env.VITE_API + '/namespaces/' + namespace,
-    ).then((response) => response.json());
+    const settings: DescribeNamespaceResponse = await requestFromAPI(
+      '/namespaces/' + namespace,
+      { request: fetch },
+    );
 
     return {
       props: { settings },
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
index b570950d..bdba38ea 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/__layout.svelte
@@ -39,7 +39,7 @@
   }
 </script>
 
-<script lang="typescript">
+<script lang="ts">
   import type { DescribeWorkflowExecutionResponse } from '$types';
 
   import { isFullScreen } from '$lib/stores/full-screen';
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/activities/[activity].svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/activities/[activity].svelte
index caf9b048..5fe003e0 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/activities/[activity].svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/activities/[activity].svelte
@@ -21,10 +21,10 @@
 </script>
 
 <script lang="ts">
-  import omit from 'lodash/omit';
   import { createEventStore } from '$lib/stores/events';
 
   import KeyValueTable from '$lib/components/key-value-table.svelte';
+  import { omit } from '$lib/utilities/omit';
 
   export let namespace: string;
   export let executionId: string;
@@ -34,7 +34,7 @@
   const { activities } = createEventStore(namespace, executionId, runId);
   $: selectedActivity = activities.getActivity(activity);
 
-  $: events = Object.keys(omit($selectedActivity, ['id', 'status']));
+  $: events = Object.keys(omit($selectedActivity, 'id', 'status'));
 </script>
 
 <div class="p-4">
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
index 4cd69694..932048a6 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/index.svelte
@@ -13,7 +13,7 @@
   }
 </script>
 
-<script lang="typescript">
+<script lang="ts">
   import { namespace } from '$lib/stores/namespace';
   import { isFullScreen } from '$lib/stores/full-screen';
 
diff --git a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/summary.svelte b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/summary.svelte
index e10dfb76..87753bf0 100644
--- a/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/summary.svelte
+++ b/src/routes/namespaces/[namespace]/workflows/[workflow]/[run]/summary.svelte
@@ -14,7 +14,7 @@
   }
 </script>
 
-<script lang="typescript">
+<script lang="ts">
   import type {
     GetWorkflowExecutionHistoryResponse,
     DescribeWorkflowExecutionResponse,


Test patch:
diff --git a/src/lib/utilities/is-function.test.ts b/src/lib/utilities/is-function.test.ts
new file mode 100644
index 00000000..7674248c
--- /dev/null
+++ b/src/lib/utilities/is-function.test.ts
@@ -0,0 +1,47 @@
+import { isFunction } from './is-function';
+
+describe(isFunction, () => {
+  it('should return true if given an arrow function', () => {
+    const fn = () => 2;
+    expect(isFunction(fn)).toBe(true);
+  });
+
+  it('should return true if given a function expression', () => {
+    const fn = function () {
+      return 2;
+    };
+    expect(isFunction(fn)).toBe(true);
+  });
+
+  it('should return true if given a function declaration', () => {
+    function fn() {
+      return 2;
+    }
+    expect(isFunction(fn)).toBe(true);
+  });
+
+  it('should return true if given an async function ', () => {
+    const fn = async () => 2;
+    expect(isFunction(fn)).toBe(true);
+  });
+
+  it('should return false if given a string', () => {
+    const fn = '';
+    expect(isFunction(fn)).toBe(false);
+  });
+
+  it('should return false if given a number', () => {
+    const fn = 42;
+    expect(isFunction(fn)).toBe(false);
+  });
+
+  it('should return false if given an object', () => {
+    const fn = {};
+    expect(isFunction(fn)).toBe(false);
+  });
+
+  it('should return false if given an array', () => {
+    const fn = [];
+    expect(isFunction(fn)).toBe(false);
+  });
+});
diff --git a/src/lib/utilities/omit.test.ts b/src/lib/utilities/omit.test.ts
new file mode 100644
index 00000000..fc41b344
--- /dev/null
+++ b/src/lib/utilities/omit.test.ts
@@ -0,0 +1,17 @@
+import { omit } from './omit';
+
+describe(omit, () => {
+  it('should remove a key from an object', () => {
+    const obj = { a: 1, b: 2 };
+    const result = omit(obj, 'b');
+
+    expect(result).toEqual({ a: 1 });
+  });
+
+  it('should not modify an object if given an invalid key', () => {
+    const obj = { a: 1, b: 2 };
+    const result = omit(obj, 'c');
+
+    expect(result).toEqual({ a: 1, b: 2 });
+  });
+});
diff --git a/src/lib/utilities/set-object-key.test.ts b/src/lib/utilities/set-object-key.test.ts
new file mode 100644
index 00000000..ca03ffb7
--- /dev/null
+++ b/src/lib/utilities/set-object-key.test.ts
@@ -0,0 +1,23 @@
+import { set } from './set-object-key';
+
+describe(set, () => {
+  it('should set an existing key', () => {
+    const obj = { a: 1 };
+    expect(set(obj, 'a', 2)).toEqual({ a: 2 });
+  });
+
+  it('should add a key to an object', () => {
+    const obj = { a: 1 };
+    expect(set(obj, 'b', 2)).toEqual({ a: 1, b: 2 });
+  });
+
+  it('should add a nested key to an object', () => {
+    const obj = { a: 1 };
+    expect(set(obj, 'b.c', 2)).toEqual({ a: 1, b: { c: 2 } });
+  });
+
+  it('should add a nested key to an object using an array', () => {
+    const obj = { a: 1 };
+    expect(set(obj, ['b', 'c'], 2)).toEqual({ a: 1, b: { c: 2 } });
+  });
+});