From 639aa29c4aaa25cbc0eff013bd779ceecb2de2cc Mon Sep 17 00:00:00 2001
From: Raphael Krut-Landau <raphael.kl@gmail.com>
Date: Thu, 11 Apr 2024 17:35:36 -0400
Subject: [PATCH] In Admin > Performance, display minimum query duration in
 seconds (#40970)

---
 .../StrategyEditorForDatabases.unit.spec.tsx  |  4 +-
 .../src/metabase-types/api/performance.ts     |  1 +
 .../components/StrategyEditorForDatabases.tsx | 10 ++++-
 .../StrategyEditorForDatabases.unit.spec.tsx  |  2 +-
 .../performance/components/StrategyForm.tsx   |  7 +++-
 .../performance/hooks/useCacheConfigs.tsx     |  6 ++-
 .../metabase/admin/performance/strategies.ts  | 38 +++++++++++++++++--
 7 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/enterprise/frontend/src/metabase-enterprise/caching/components/StrategyEditorForDatabases.unit.spec.tsx b/enterprise/frontend/src/metabase-enterprise/caching/components/StrategyEditorForDatabases.unit.spec.tsx
index e846565d387..a4c559e76fa 100644
--- a/enterprise/frontend/src/metabase-enterprise/caching/components/StrategyEditorForDatabases.unit.spec.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/caching/components/StrategyEditorForDatabases.unit.spec.tsx
@@ -73,7 +73,7 @@ describe("StrategyEditorForDatabases", () => {
     expect(await getSaveButton()).toBeInTheDocument();
 
     await act(async () => {
-      await changeInput(/minimum query duration/i, 60000, 70000);
+      await changeInput(/minimum query duration/i, 1, 5);
       await changeInput(/multiplier/i, 10, 3);
     });
 
@@ -136,7 +136,7 @@ describe("StrategyEditorForDatabases", () => {
     expect((await screen.findAllByRole("spinbutton")).length).toBe(2);
 
     await act(async () => {
-      await changeInput(/minimum query duration/i, 60000, 70000);
+      await changeInput(/minimum query duration/i, 1, 5);
       await changeInput(/multiplier/i, 10, 3);
     });
 
diff --git a/frontend/src/metabase-types/api/performance.ts b/frontend/src/metabase-types/api/performance.ts
index 3a4730c9c9c..ffd7fab62c5 100644
--- a/frontend/src/metabase-types/api/performance.ts
+++ b/frontend/src/metabase-types/api/performance.ts
@@ -17,6 +17,7 @@ export interface TTLStrategy extends StrategyBase {
   type: "ttl";
   multiplier: number;
   min_duration_ms: number;
+  min_duration_seconds?: number;
 }
 
 export interface DoNotCacheStrategy extends StrategyBase {
diff --git a/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.tsx b/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.tsx
index 1a3b5d2b393..4c115e34068 100644
--- a/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.tsx
+++ b/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.tsx
@@ -17,7 +17,12 @@ import { useCacheConfigs } from "../hooks/useCacheConfigs";
 import { useConfirmOnRouteLeave } from "../hooks/useConfirmOnRouteLeave";
 import { useVerticallyOverflows } from "../hooks/useVerticallyOverflows";
 import type { UpdateTargetId } from "../strategies";
-import { getFieldsForStrategyType, rootId, Strategies } from "../strategies";
+import {
+  getFieldsForStrategyType,
+  rootId,
+  Strategies,
+  translateConfigToAPI,
+} from "../strategies";
 
 import { Panel, TabWrapper } from "./StrategyEditorForDatabases.styled";
 import { StrategyForm } from "./StrategyForm";
@@ -137,7 +142,8 @@ const StrategyEditorForDatabases_Base = ({
           strategy: validatedStrategy,
         };
 
-        await CacheConfigApi.update(newConfig);
+        const translatedConfig = translateConfigToAPI(newConfig);
+        await CacheConfigApi.update(translatedConfig);
         setConfigs([...otherConfigs, newConfig]);
       }
     },
diff --git a/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.unit.spec.tsx b/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.unit.spec.tsx
index 136ec297d22..674b24c3bdd 100644
--- a/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.unit.spec.tsx
+++ b/frontend/src/metabase/admin/performance/components/StrategyEditorForDatabases.unit.spec.tsx
@@ -19,7 +19,7 @@ describe("StrategyEditorForDatabases", () => {
     expect(await getSaveButton()).toBeInTheDocument();
 
     await act(async () => {
-      await changeInput(/minimum query duration/i, 60000, 70000);
+      await changeInput(/minimum query duration/i, 1, 5);
       await changeInput(/multiplier/i, 10, 3);
     });
 
diff --git a/frontend/src/metabase/admin/performance/components/StrategyForm.tsx b/frontend/src/metabase/admin/performance/components/StrategyForm.tsx
index 2d83fb556cb..d8982a84ac4 100644
--- a/frontend/src/metabase/admin/performance/components/StrategyForm.tsx
+++ b/frontend/src/metabase/admin/performance/components/StrategyForm.tsx
@@ -104,9 +104,12 @@ const StrategyFormBody = ({
           <>
             <Field
               title={t`Minimum query duration`}
-              subtitle={t`Metabase will cache all saved questions with an average query execution time greater than this many milliseconds.`}
+              subtitle={t`Metabase will cache all saved questions with an average query execution time greater than this many seconds.`}
             >
-              <PositiveNumberInput strategyType="ttl" name="min_duration_ms" />
+              <PositiveNumberInput
+                strategyType="ttl"
+                name="min_duration_seconds"
+              />
             </Field>
             <Field
               title={t`Cache time-to-live (TTL) multiplier`}
diff --git a/frontend/src/metabase/admin/performance/hooks/useCacheConfigs.tsx b/frontend/src/metabase/admin/performance/hooks/useCacheConfigs.tsx
index d1d29e08b87..fd0cdade7d2 100644
--- a/frontend/src/metabase/admin/performance/hooks/useCacheConfigs.tsx
+++ b/frontend/src/metabase/admin/performance/hooks/useCacheConfigs.tsx
@@ -5,7 +5,7 @@ import { useDatabaseListQuery } from "metabase/common/hooks";
 import { CacheConfigApi } from "metabase/services";
 import type { CacheConfigAPIResponse, Config } from "metabase-types/api";
 
-import { rootId } from "../strategies";
+import { rootId, translateConfigFromAPI } from "../strategies";
 
 import { useRecentlyTrue } from "./useRecentlyTrue";
 
@@ -28,7 +28,9 @@ export const useCacheConfigs = ({
           })) as CacheConfigAPIResponse
         ).data
       : [];
-    const configs = [...rootConfigsFromAPI, ...dbConfigsFromAPI];
+    const configs = [...rootConfigsFromAPI, ...dbConfigsFromAPI].map(config =>
+      translateConfigFromAPI(config),
+    );
     return configs;
   }, []);
 
diff --git a/frontend/src/metabase/admin/performance/strategies.ts b/frontend/src/metabase/admin/performance/strategies.ts
index 7e4307c549b..2bb3edf3bee 100644
--- a/frontend/src/metabase/admin/performance/strategies.ts
+++ b/frontend/src/metabase/admin/performance/strategies.ts
@@ -3,7 +3,7 @@ import type { AnySchema } from "yup";
 import * as Yup from "yup";
 import type { SchemaObjectDescription } from "yup/lib/schema";
 
-import type { Strategy, StrategyType } from "metabase-types/api";
+import type { Config, Strategy, StrategyType } from "metabase-types/api";
 import { DurationUnit } from "metabase-types/api";
 
 export type UpdateTargetId = (
@@ -22,8 +22,8 @@ export const rootId = 0;
 const durationUnits = new Set(Object.values(DurationUnit).map(String));
 
 const positiveInteger = Yup.number()
-  .positive(t`The minimum query duration must be a positive number.`)
-  .integer(t`The minimum query duration must be an integer.`);
+  .positive(t`Enter a positive number.`)
+  .integer(t`Enter an integer.`);
 
 export const inheritStrategyValidationSchema = Yup.object({
   type: Yup.string().equals(["inherit"]),
@@ -33,9 +33,13 @@ export const doNotCacheStrategyValidationSchema = Yup.object({
   type: Yup.string().equals(["nocache"]),
 });
 
+export const defaultMinDurationMs = 1000;
 export const ttlStrategyValidationSchema = Yup.object({
   type: Yup.string().equals(["ttl"]),
-  min_duration_ms: positiveInteger.default(60000),
+  min_duration_ms: positiveInteger.default(defaultMinDurationMs),
+  min_duration_seconds: positiveInteger.default(
+    Math.ceil(defaultMinDurationMs / 1000),
+  ),
   multiplier: positiveInteger.default(10),
 });
 
@@ -126,3 +130,29 @@ export const getFieldsForStrategyType = (strategyType: StrategyType) => {
   const fields = Object.keys(fieldRecord);
   return fields;
 };
+
+export const translateConfig = (
+  config: Config,
+  direction: "fromAPI" | "toAPI",
+): Config => {
+  const translated: Config = { ...config };
+  if (translated.strategy.type === "ttl") {
+    if (direction === "fromAPI") {
+      translated.strategy.min_duration_seconds = Math.ceil(
+        translated.strategy.min_duration_ms / 1000,
+      );
+    } else {
+      translated.strategy.min_duration_ms =
+        translated.strategy.min_duration_seconds === undefined
+          ? defaultMinDurationMs
+          : translated.strategy.min_duration_seconds * 1000;
+      delete translated.strategy.min_duration_seconds;
+    }
+  }
+  return translated;
+};
+
+export const translateConfigFromAPI = (config: Config): Config =>
+  translateConfig(config, "fromAPI");
+export const translateConfigToAPI = (config: Config): Config =>
+  translateConfig(config, "toAPI");
-- 
GitLab