From 7022541d4bf167ec40c32c3abbdb029de8379771 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Thu, 1 Dec 2022 18:10:46 +0200
Subject: [PATCH] Add schedule fields and default engine to the formik database
 form (#26856)

---
 .../src/metabase-types/store/mocks/setup.ts   |   3 +
 frontend/src/metabase-types/store/setup.ts    |   3 +
 .../SchedulePicker/SchedulePicker.tsx         |   2 +-
 .../metabase/containers/SchedulePicker.tsx    |   2 +-
 .../core/components/FormSelect/FormSelect.tsx |  10 +-
 .../core/components/FormToggle/FormToggle.tsx |  25 +++-
 .../DatabaseCacheScheduleField.styled.tsx     |  74 ++++++++++
 .../DatabaseCacheScheduleField.tsx            | 138 ++++++++++++++++++
 .../DatabaseCacheScheduleField/index.ts       |   1 +
 .../DatabaseEngineField.tsx                   |   8 +-
 .../DatabaseForm/DatabaseForm.stories.tsx     |  81 +++++++++-
 .../components/DatabaseForm/DatabaseForm.tsx  |  36 +++--
 .../DatabaseScheduleToggleField.tsx           |  43 ++++++
 .../DatabaseScheduleToggleField/index.ts      |   1 +
 .../DatabaseSyncScheduleField.tsx             |  50 +++++++
 .../DatabaseSyncScheduleField/index.ts        |   1 +
 frontend/src/metabase/databases/constants.tsx |   8 +
 .../containers/DatabaseForm/DatabaseForm.tsx  |   5 +-
 frontend/src/metabase/databases/types.ts      |   7 +-
 .../src/metabase/databases/utils/engine.ts    |   4 +
 .../src/metabase/databases/utils/schema.ts    |   3 +
 21 files changed, 471 insertions(+), 34 deletions(-)
 create mode 100644 frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.styled.tsx
 create mode 100644 frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.tsx
 create mode 100644 frontend/src/metabase/databases/components/DatabaseCacheScheduleField/index.ts
 create mode 100644 frontend/src/metabase/databases/components/DatabaseScheduleToggleField/DatabaseScheduleToggleField.tsx
 create mode 100644 frontend/src/metabase/databases/components/DatabaseScheduleToggleField/index.ts
 create mode 100644 frontend/src/metabase/databases/components/DatabaseSyncScheduleField/DatabaseSyncScheduleField.tsx
 create mode 100644 frontend/src/metabase/databases/components/DatabaseSyncScheduleField/index.ts

diff --git a/frontend/src/metabase-types/store/mocks/setup.ts b/frontend/src/metabase-types/store/mocks/setup.ts
index c0f28e1e880..08284538d1b 100644
--- a/frontend/src/metabase-types/store/mocks/setup.ts
+++ b/frontend/src/metabase-types/store/mocks/setup.ts
@@ -41,6 +41,9 @@ export const createMockDatabaseInfo = (
   schedules: {},
   auto_run_queries: false,
   refingerprint: false,
+  is_sample: false,
+  is_full_sync: false,
+  is_on_demand: false,
   ...opts,
 });
 
diff --git a/frontend/src/metabase-types/store/setup.ts b/frontend/src/metabase-types/store/setup.ts
index 45858c5dab7..e28a057403f 100644
--- a/frontend/src/metabase-types/store/setup.ts
+++ b/frontend/src/metabase-types/store/setup.ts
@@ -27,6 +27,9 @@ export interface DatabaseInfo {
   schedules: DatabaseSchedules;
   auto_run_queries: boolean;
   refingerprint: boolean;
+  is_sample: boolean;
+  is_full_sync: boolean;
+  is_on_demand: boolean;
 }
 
 export interface SubscribeInfo {
diff --git a/frontend/src/metabase/components/SchedulePicker/SchedulePicker.tsx b/frontend/src/metabase/components/SchedulePicker/SchedulePicker.tsx
index 47db0d86cc1..60d9c693bd0 100644
--- a/frontend/src/metabase/components/SchedulePicker/SchedulePicker.tsx
+++ b/frontend/src/metabase/components/SchedulePicker/SchedulePicker.tsx
@@ -66,7 +66,7 @@ type ScheduleChangeProp = { name: ScheduleProperty; value: unknown };
 export interface SchedulePickerProps {
   schedule: ScheduleSettings;
   scheduleOptions: ScheduleType[];
-  timezone: string;
+  timezone?: string;
   textBeforeInterval?: string;
   textBeforeSendTime?: string;
   minutesOnHourPicker?: boolean;
diff --git a/frontend/src/metabase/containers/SchedulePicker.tsx b/frontend/src/metabase/containers/SchedulePicker.tsx
index 0ba5a083e31..7c24658f383 100644
--- a/frontend/src/metabase/containers/SchedulePicker.tsx
+++ b/frontend/src/metabase/containers/SchedulePicker.tsx
@@ -6,7 +6,7 @@ import SchedulePicker, {
 } from "metabase/components/SchedulePicker";
 
 type StateProps = {
-  timezone: string;
+  timezone?: string;
 };
 
 function mapStateToProps(state: State): StateProps {
diff --git a/frontend/src/metabase/core/components/FormSelect/FormSelect.tsx b/frontend/src/metabase/core/components/FormSelect/FormSelect.tsx
index 0dd14524e9c..919319699b7 100644
--- a/frontend/src/metabase/core/components/FormSelect/FormSelect.tsx
+++ b/frontend/src/metabase/core/components/FormSelect/FormSelect.tsx
@@ -24,21 +24,21 @@ const FormSelect = forwardRef(function FormSelect<
     className,
     title,
     description,
-    onChange: onChangeProp,
+    onChange,
     ...props
   }: FormSelectProps<TValue, TOption>,
   ref: Ref<HTMLDivElement>,
 ) {
   const id = useUniqueId();
-  const [{ value, onChange, onBlur }, { error, touched }] = useField(name);
+  const [{ value, onBlur }, { error, touched }, { setValue }] = useField(name);
   const buttonProps = useMemo(() => ({ id, onBlur }), [id, onBlur]);
 
   const handleChange = useCallback(
     (event: SelectChangeEvent<TValue>) => {
-      onChange(event);
-      onChangeProp?.(event);
+      setValue(event.target.value);
+      onChange?.(event);
     },
-    [onChange, onChangeProp],
+    [setValue, onChange],
   );
 
   return (
diff --git a/frontend/src/metabase/core/components/FormToggle/FormToggle.tsx b/frontend/src/metabase/core/components/FormToggle/FormToggle.tsx
index 807fc5ff55c..9ea59d5ad51 100644
--- a/frontend/src/metabase/core/components/FormToggle/FormToggle.tsx
+++ b/frontend/src/metabase/core/components/FormToggle/FormToggle.tsx
@@ -1,23 +1,38 @@
-import React, { forwardRef, ReactNode, Ref } from "react";
+import React, { forwardRef, ReactNode, Ref, useCallback } from "react";
 import { useField } from "formik";
 import { useUniqueId } from "metabase/hooks/use-unique-id";
 import Toggle, { ToggleProps } from "metabase/core/components/Toggle";
 import FormField from "metabase/core/components/FormField";
 
-export interface FormToggleProps
-  extends Omit<ToggleProps, "value" | "onChange" | "onBlur"> {
+export interface FormToggleProps extends Omit<ToggleProps, "value" | "onBlur"> {
   name: string;
   title?: string;
   description?: ReactNode;
 }
 
 const FormToggle = forwardRef(function FormToggle(
-  { name, className, style, title, description, ...props }: FormToggleProps,
+  {
+    name,
+    className,
+    style,
+    title,
+    description,
+    onChange,
+    ...props
+  }: FormToggleProps,
   ref: Ref<HTMLDivElement>,
 ) {
   const id = useUniqueId();
   const [{ value, onBlur }, { error, touched }, { setValue }] = useField(name);
 
+  const handleChange = useCallback(
+    (value: boolean) => {
+      setValue(value);
+      onChange?.(value);
+    },
+    [setValue, onChange],
+  );
+
   return (
     <FormField
       ref={ref}
@@ -34,7 +49,7 @@ const FormToggle = forwardRef(function FormToggle(
         id={id}
         name={name}
         value={value ?? false}
-        onChange={setValue}
+        onChange={handleChange}
         onBlur={onBlur}
       />
     </FormField>
diff --git a/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.styled.tsx b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.styled.tsx
new file mode 100644
index 00000000000..12e4090153d
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.styled.tsx
@@ -0,0 +1,74 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+
+export const ScheduleOptionList = styled.div`
+  border: 1px solid ${color("border")};
+  box-shadow: 0 2px 2px ${color("shadow")};
+`;
+
+interface ScheduleOptionRootProps {
+  isSelected: boolean;
+}
+
+export const ScheduleOptionRoot = styled.div<ScheduleOptionRootProps>`
+  display: flex;
+  cursor: ${props => !props.isSelected && "pointer"};
+  padding: 1.5rem 1rem;
+  border-bottom: 1px solid ${color("border")};
+
+  &:last-child {
+    border-bottom: none;
+  }
+`;
+
+interface ScheduleOptionIndicatorProps {
+  isSelected: boolean;
+}
+
+export const ScheduleOptionIndicator = styled.div<ScheduleOptionIndicatorProps>`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 1.125rem;
+  height: 1.125rem;
+  border: 0.125rem solid
+    ${props => (props.isSelected ? color("brand") : color("text-light"))};
+  border-radius: 50%;
+`;
+
+interface ScheduleOptionIndicatorBackgroundProps {
+  isSelected: boolean;
+}
+
+export const ScheduleOptionIndicatorBackground = styled.div<ScheduleOptionIndicatorBackgroundProps>`
+  width: 0.5rem;
+  height: 0.5rem;
+  border-radius: 50%;
+  background-color: ${props => props.isSelected && color("brand")};
+`;
+
+export const ScheduleOptionBody = styled.div`
+  margin-left: 1rem;
+`;
+
+interface ScheduleOptionTitleProps {
+  isSelected: boolean;
+}
+
+export const ScheduleOptionTitle = styled.div<ScheduleOptionTitleProps>`
+  color: ${props => (props.isSelected ? color("brand") : color("text-medium"))};
+  font-size: 1rem;
+  font-weight: bold;
+  line-height: 1.25rem;
+`;
+
+export const ScheduleOptionContent = styled.div`
+  margin-top: 1rem;
+`;
+
+export const ScheduleOptionText = styled.div`
+  color: ${color("text-medium")};
+  font-size: 1rem;
+  line-height: 1.5rem;
+  max-width: 38.75rem;
+`;
diff --git a/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.tsx b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.tsx
new file mode 100644
index 00000000000..9f963d28636
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/DatabaseCacheScheduleField.tsx
@@ -0,0 +1,138 @@
+import React, { ReactNode, useCallback } from "react";
+import { useField, useFormikContext } from "formik";
+import { t } from "ttag";
+import FormField from "metabase/core/components/FormField";
+import SchedulePicker from "metabase/components/SchedulePicker";
+import { ScheduleSettings, ScheduleType } from "metabase-types/api";
+import { DatabaseValues } from "../../types";
+import {
+  ScheduleOptionList,
+  ScheduleOptionBody,
+  ScheduleOptionContent,
+  ScheduleOptionIndicator,
+  ScheduleOptionIndicatorBackground,
+  ScheduleOptionRoot,
+  ScheduleOptionTitle,
+  ScheduleOptionText,
+} from "./DatabaseCacheScheduleField.styled";
+
+const DEFAULT_SCHEDULE: ScheduleSettings = {
+  schedule_day: "mon",
+  schedule_frame: null,
+  schedule_hour: 0,
+  schedule_type: "daily",
+};
+
+const SCHEDULE_OPTIONS: ScheduleType[] = ["daily", "weekly", "monthly"];
+
+export interface DatabaseCacheScheduleFieldProps {
+  name: string;
+  title?: string;
+  description?: ReactNode;
+}
+
+const DatabaseCacheScheduleField = ({
+  name,
+  title,
+  description,
+}: DatabaseCacheScheduleFieldProps): JSX.Element => {
+  const { values, setValues } = useFormikContext<DatabaseValues>();
+  const [{ value }, , { setValue }] = useField(name);
+
+  const handleScheduleChange = useCallback(
+    (value: ScheduleSettings) => {
+      setValue(value);
+    },
+    [setValue],
+  );
+
+  const handleFullSyncSelect = useCallback(() => {
+    setValues(values => ({
+      ...values,
+      is_full_sync: true,
+      is_on_demand: false,
+    }));
+  }, [setValues]);
+
+  const handleOnDemandSyncSelect = useCallback(() => {
+    setValues(values => ({
+      ...values,
+      schedules: {},
+      is_full_sync: false,
+      is_on_demand: true,
+    }));
+  }, [setValues]);
+
+  const handleNoneSyncSelect = useCallback(() => {
+    setValues(values => ({
+      ...values,
+      schedules: {},
+      is_full_sync: false,
+      is_on_demand: false,
+    }));
+  }, [setValues]);
+
+  return (
+    <FormField title={title} description={description}>
+      <ScheduleOptionList>
+        <ScheduleOption
+          title={t`Regularly, on a schedule`}
+          isSelected={values.is_full_sync}
+          onSelect={handleFullSyncSelect}
+        >
+          <SchedulePicker
+            schedule={value ?? DEFAULT_SCHEDULE}
+            scheduleOptions={SCHEDULE_OPTIONS}
+            onScheduleChange={handleScheduleChange}
+          />
+        </ScheduleOption>
+        <ScheduleOption
+          title={t`Only when adding a new filter widget`}
+          isSelected={!values.is_full_sync && values.is_on_demand}
+          onSelect={handleOnDemandSyncSelect}
+        >
+          <ScheduleOptionText>
+            {t`When a user adds a new filter to a dashboard or a SQL question, Metabase will scan the field(s) mapped to that filter in order to show the list of selectable values.`}
+          </ScheduleOptionText>
+        </ScheduleOption>
+        <ScheduleOption
+          title={t`Never, I'll do this manually if I need to`}
+          isSelected={!values.is_full_sync && !values.is_on_demand}
+          onSelect={handleNoneSyncSelect}
+        />
+      </ScheduleOptionList>
+    </FormField>
+  );
+};
+
+interface ScheduleOptionProps {
+  title: string;
+  isSelected: boolean;
+  children?: ReactNode;
+  onSelect: () => void;
+}
+
+const ScheduleOption = ({
+  title,
+  isSelected,
+  children,
+  onSelect,
+}: ScheduleOptionProps): JSX.Element => {
+  return (
+    <ScheduleOptionRoot isSelected={isSelected} onClick={onSelect}>
+      <ScheduleOptionIndicator isSelected={isSelected}>
+        <ScheduleOptionIndicatorBackground isSelected={isSelected} />
+      </ScheduleOptionIndicator>
+      <ScheduleOptionBody>
+        <ScheduleOptionTitle isSelected={isSelected}>
+          {title}
+        </ScheduleOptionTitle>
+        {children && isSelected && (
+          <ScheduleOptionContent>{children}</ScheduleOptionContent>
+        )}
+      </ScheduleOptionBody>
+    </ScheduleOptionRoot>
+  );
+};
+
+export default DatabaseCacheScheduleField;
diff --git a/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/index.ts b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/index.ts
new file mode 100644
index 00000000000..62bfa5f0d7f
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseCacheScheduleField/index.ts
@@ -0,0 +1 @@
+export { default } from "./DatabaseCacheScheduleField";
diff --git a/frontend/src/metabase/databases/components/DatabaseEngineField/DatabaseEngineField.tsx b/frontend/src/metabase/databases/components/DatabaseEngineField/DatabaseEngineField.tsx
index 8350db138cd..5e302ca0508 100644
--- a/frontend/src/metabase/databases/components/DatabaseEngineField/DatabaseEngineField.tsx
+++ b/frontend/src/metabase/databases/components/DatabaseEngineField/DatabaseEngineField.tsx
@@ -1,5 +1,7 @@
-import React, { useMemo, useRef } from "react";
+import React, { useMemo } from "react";
+import { useFormikContext } from "formik";
 import { Engine } from "metabase-types/api";
+import { DatabaseValues } from "../../types";
 import { getEngineOptions } from "../../utils/engine";
 import DatabaseEngineSelect from "./DatabaseEngineSelect";
 import DatabaseEngineWidget from "./DatabaseEngineWidget";
@@ -19,7 +21,7 @@ const DatabaseEngineField = ({
   isAdvanced,
   onChange,
 }: DatabaseEngineFieldProps): JSX.Element => {
-  const { current: isNew } = useRef(engineKey == null);
+  const { values } = useFormikContext<DatabaseValues>();
 
   const options = useMemo(() => {
     return getEngineOptions(engines, engineKey);
@@ -28,7 +30,7 @@ const DatabaseEngineField = ({
   return isAdvanced ? (
     <DatabaseEngineSelect
       options={options}
-      disabled={!isNew}
+      disabled={values.is_sample}
       onChange={onChange}
     />
   ) : (
diff --git a/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.stories.tsx b/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.stories.tsx
index 852ee4fb795..018c265ff5b 100644
--- a/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.stories.tsx
+++ b/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.stories.tsx
@@ -15,12 +15,79 @@ const Template: ComponentStory<typeof DatabaseForm> = args => {
 export const Default = Template.bind({});
 Default.args = {
   engines: {
-    presto: createMockEngine({
-      "driver-name": "Presto (Deprecated Driver)",
-      "superseded-by": "presto-jdbc",
-    }),
-    "presto-jdbc": createMockEngine({
-      "driver-name": "Presto",
-    }),
+    h2: {
+      source: {
+        type: "official",
+        contact: null,
+      },
+      "details-fields": [
+        {
+          name: "db",
+          "display-name": "Connection String",
+          "helper-text":
+            "The local path relative to where Metabase is running from. Your string should not include the .mv.db extension.",
+          placeholder: "file:/Users/camsaul/bird_sightings/toucans",
+          required: true,
+        },
+        {
+          name: "advanced-options",
+          type: "section",
+          default: false,
+        },
+        {
+          name: "auto_run_queries",
+          type: "boolean",
+          default: true,
+          "display-name": "Rerun queries for simple explorations",
+          description:
+            "We execute the underlying query when you explore data using Summarize or Filter. This is on by default but you can turn it off if performance is slow.",
+          "visible-if": {
+            "advanced-options": true,
+          },
+        },
+        {
+          name: "let-user-control-scheduling",
+          type: "boolean",
+          "display-name": "Choose when syncs and scans happen",
+          description:
+            "By default, Metabase does a lightweight hourly sync and an intensive daily scan of field values. If you have a large database, turn this on to make changes.",
+          "visible-if": {
+            "advanced-options": true,
+          },
+        },
+        {
+          name: "schedules.metadata_sync",
+          "display-name": "Database syncing",
+          description:
+            "This is a lightweight process that checks for updates to this database’s schema. In most cases, you should be fine leaving this set to sync hourly.",
+          "visible-if": {
+            "advanced-options": true,
+            "let-user-control-scheduling": true,
+          },
+        },
+        {
+          name: "schedules.cache_field_values",
+          "display-name": "Scanning for Filter Values",
+          description:
+            "Metabase can scan the values present in each field in this database to enable checkbox filters in dashboards and questions. This can be a somewhat resource-intensive process, particularly if you have a very large database. When should Metabase automatically scan and cache field values?",
+          "visible-if": {
+            "advanced-options": true,
+            "let-user-control-scheduling": true,
+          },
+        },
+        {
+          name: "refingerprint",
+          type: "boolean",
+          "display-name": "Periodically refingerprint tables",
+          description:
+            "This enables Metabase to scan for additional field values during syncs allowing smarter behavior, like improved auto-binning on your bar charts.",
+          "visible-if": {
+            "advanced-options": true,
+          },
+        },
+      ],
+      "driver-name": "H2",
+      "superseded-by": null,
+    },
   },
 };
diff --git a/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.tsx b/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.tsx
index 2f27e43cacb..3013bb1e69f 100644
--- a/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.tsx
+++ b/frontend/src/metabase/databases/components/DatabaseForm/DatabaseForm.tsx
@@ -9,6 +9,7 @@ import FormSubmitButton from "metabase/core/components/FormSubmitButton";
 import FormErrorMessage from "metabase/core/components/FormErrorMessage";
 import { Engine } from "metabase-types/api";
 import { DatabaseValues } from "../../types";
+import { getDefaultEngineKey } from "../../utils/engine";
 import { getValidationSchema, getVisibleFields } from "../../utils/schema";
 import DatabaseEngineField from "../DatabaseEngineField";
 import DatabaseNameField from "../DatabaseNameField";
@@ -35,7 +36,9 @@ const DatabaseForm = ({
   onCancel,
   onEngineChange,
 }: DatabaseFormProps): JSX.Element => {
-  const [engineKey, setEngineKey] = useState(initialData?.engine);
+  const [engineKey, setEngineKey] = useState(() =>
+    getInitialEngineKey(engines, initialData, isAdvanced),
+  );
   const engine = engineKey ? engines[engineKey] : undefined;
 
   const validationSchema = useMemo(() => {
@@ -110,13 +113,11 @@ const DatabaseFormBody = ({
         isAdvanced={isAdvanced}
         onChange={onEngineChange}
       />
-      {!isAdvanced && (
-        <DatabaseEngineWarning
-          engineKey={engineKey}
-          engines={engines}
-          onChange={onEngineChange}
-        />
-      )}
+      <DatabaseEngineWarning
+        engineKey={engineKey}
+        engines={engines}
+        onChange={onEngineChange}
+      />
       {engine && <DatabaseNameField engine={engine} />}
       {fields.map(field => (
         <DatabaseDetailField key={field.name} field={field} />
@@ -136,11 +137,16 @@ const DatabaseFormFooter = ({
   onCancel,
 }: DatabaseFormFooterProps) => {
   const { values, dirty } = useFormikContext<DatabaseValues>();
+  const isNew = values.id == null;
 
   if (isAdvanced) {
     return (
       <div>
-        <FormSubmitButton title={t`Save`} disabled={!dirty} primary />
+        <FormSubmitButton
+          title={isNew ? t`Save` : t`Save changes`}
+          disabled={!dirty}
+          primary
+        />
         <FormErrorMessage />
       </div>
     );
@@ -163,4 +169,16 @@ const DatabaseFormFooter = ({
   }
 };
 
+const getInitialEngineKey = (
+  engines: Record<string, Engine>,
+  values?: DatabaseValues,
+  isAdvanced?: boolean,
+) => {
+  if (values?.engine) {
+    return values.engine;
+  } else if (isAdvanced) {
+    return getDefaultEngineKey(engines);
+  }
+};
+
 export default DatabaseForm;
diff --git a/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/DatabaseScheduleToggleField.tsx b/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/DatabaseScheduleToggleField.tsx
new file mode 100644
index 00000000000..2d0e010aabe
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/DatabaseScheduleToggleField.tsx
@@ -0,0 +1,43 @@
+import React, { ReactNode, useCallback } from "react";
+import { useFormikContext } from "formik";
+import FormToggle from "metabase/core/components/FormToggle";
+import { DatabaseValues } from "../../types";
+
+export interface DatabaseScheduleToggleFieldProps {
+  name: string;
+  title?: string;
+  description?: ReactNode;
+}
+
+const DatabaseScheduleToggleField = ({
+  name,
+  title,
+  description,
+}: DatabaseScheduleToggleFieldProps): JSX.Element => {
+  const { setValues } = useFormikContext<DatabaseValues>();
+
+  const handleChange = useCallback(
+    (value: boolean) => {
+      if (!value) {
+        setValues(values => ({
+          ...values,
+          schedules: {},
+          is_full_sync: true,
+          is_on_demand: false,
+        }));
+      }
+    },
+    [setValues],
+  );
+
+  return (
+    <FormToggle
+      name={name}
+      title={title}
+      description={description}
+      onChange={handleChange}
+    />
+  );
+};
+
+export default DatabaseScheduleToggleField;
diff --git a/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/index.ts b/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/index.ts
new file mode 100644
index 00000000000..0862e6b41c7
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseScheduleToggleField/index.ts
@@ -0,0 +1 @@
+export { default } from "./DatabaseScheduleToggleField";
diff --git a/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/DatabaseSyncScheduleField.tsx b/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/DatabaseSyncScheduleField.tsx
new file mode 100644
index 00000000000..a009409f075
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/DatabaseSyncScheduleField.tsx
@@ -0,0 +1,50 @@
+import React, { ReactNode, useCallback } from "react";
+import { useField } from "formik";
+import { t } from "ttag";
+import SchedulePicker from "metabase/components/SchedulePicker";
+import { ScheduleSettings, ScheduleType } from "metabase-types/api";
+import FormField from "metabase/core/components/FormField";
+
+const DEFAULT_SCHEDULE: ScheduleSettings = {
+  schedule_day: "mon",
+  schedule_frame: null,
+  schedule_hour: 0,
+  schedule_type: "daily",
+};
+
+const SCHEDULE_OPTIONS: ScheduleType[] = ["hourly", "daily"];
+
+export interface DatabaseSyncScheduleFieldProps {
+  name: string;
+  title?: string;
+  description?: ReactNode;
+}
+
+const DatabaseSyncScheduleField = ({
+  name,
+  title,
+  description,
+}: DatabaseSyncScheduleFieldProps): JSX.Element => {
+  const [{ value }, , { setValue }] = useField(name);
+
+  const handleScheduleChange = useCallback(
+    (value: ScheduleSettings) => {
+      setValue(value);
+    },
+    [setValue],
+  );
+
+  return (
+    <FormField title={title} description={description}>
+      <SchedulePicker
+        schedule={value ?? DEFAULT_SCHEDULE}
+        scheduleOptions={SCHEDULE_OPTIONS}
+        textBeforeInterval={t`Scan`}
+        minutesOnHourPicker
+        onScheduleChange={handleScheduleChange}
+      />
+    </FormField>
+  );
+};
+
+export default DatabaseSyncScheduleField;
diff --git a/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/index.ts b/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/index.ts
new file mode 100644
index 00000000000..8439442836d
--- /dev/null
+++ b/frontend/src/metabase/databases/components/DatabaseSyncScheduleField/index.ts
@@ -0,0 +1 @@
+export { default } from "./DatabaseSyncScheduleField";
diff --git a/frontend/src/metabase/databases/constants.tsx b/frontend/src/metabase/databases/constants.tsx
index 3f7ca3ecaae..a06df0c091b 100644
--- a/frontend/src/metabase/databases/constants.tsx
+++ b/frontend/src/metabase/databases/constants.tsx
@@ -1,10 +1,13 @@
 import React from "react";
 import { t } from "ttag";
 import DatabaseAuthCodeDescription from "./components/DatabaseAuthCodeDescription";
+import DatabaseCacheScheduleField from "./components/DatabaseCacheScheduleField";
 import DatabaseClientIdDescription from "./components/DatabaseClientIdDescription";
 import DatabaseConnectionSectionField from "./components/DatabaseConnectionSectionField";
+import DatabaseScheduleToggleField from "./components/DatabaseScheduleToggleField";
 import DatabaseSshDescription from "./components/DatabaseSshDescription";
 import DatabaseSslKeyDescription from "./components/DatabaseSslKeyDescription";
+import DatabaseSyncScheduleField from "./components/DatabaseSyncScheduleField";
 import { EngineFieldOverride } from "./types";
 
 export const ELEVATED_ENGINES = [
@@ -92,11 +95,16 @@ export const FIELD_OVERRIDES: Record<string, EngineFieldOverride> = {
   "use-conn-uri": {
     type: DatabaseConnectionSectionField,
   },
+  "let-user-control-scheduling": {
+    type: DatabaseScheduleToggleField,
+  },
   "schedules.metadata_sync": {
     name: "schedules.metadata_sync",
+    type: DatabaseSyncScheduleField,
   },
   "schedules.cache_field_values": {
     name: "schedules.cache_field_values",
+    type: DatabaseCacheScheduleField,
   },
   auto_run_queries: {
     name: "auto_run_queries",
diff --git a/frontend/src/metabase/databases/containers/DatabaseForm/DatabaseForm.tsx b/frontend/src/metabase/databases/containers/DatabaseForm/DatabaseForm.tsx
index eaab029de55..988b47982a6 100644
--- a/frontend/src/metabase/databases/containers/DatabaseForm/DatabaseForm.tsx
+++ b/frontend/src/metabase/databases/containers/DatabaseForm/DatabaseForm.tsx
@@ -3,8 +3,9 @@ import { getSetting } from "metabase/selectors/settings";
 import { State } from "metabase-types/store";
 import DatabaseForm, { DatabaseFormProps } from "../../components/DatabaseForm";
 
-type DatabaseFormOwnProps = Omit<DatabaseFormProps, "engines" | "isHosted">;
-type DatabaseFormStateProps = Pick<DatabaseFormProps, "engines" | "isHosted">;
+type DatabaseFormStateKeys = "engines" | "isHosted";
+type DatabaseFormOwnProps = Omit<DatabaseFormProps, DatabaseFormStateKeys>;
+type DatabaseFormStateProps = Pick<DatabaseFormProps, DatabaseFormStateKeys>;
 
 const mapStateToProps = (state: State) => ({
   engines: getSetting(state, "engines"),
diff --git a/frontend/src/metabase/databases/types.ts b/frontend/src/metabase/databases/types.ts
index e764f02ac61..c2a49942ffa 100644
--- a/frontend/src/metabase/databases/types.ts
+++ b/frontend/src/metabase/databases/types.ts
@@ -1,17 +1,22 @@
 import { ComponentType, ReactNode } from "react";
 import {
+  DatabaseId,
   DatabaseSchedules,
   EngineFieldOption,
   EngineFieldType,
 } from "metabase-types/api";
 
 export interface DatabaseValues {
+  id?: DatabaseId;
   name: string;
   engine: string | undefined;
   details: Record<string, unknown>;
   schedules: DatabaseSchedules;
   auto_run_queries: boolean;
   refingerprint: boolean;
+  is_sample: boolean;
+  is_full_sync: boolean;
+  is_on_demand: boolean;
 }
 
 export interface EngineOption {
@@ -21,8 +26,8 @@ export interface EngineOption {
 }
 
 export interface EngineFieldOverride {
-  type?: EngineFieldType | ComponentType<EngineFieldProps>;
   name?: string;
+  type?: EngineFieldType | ComponentType<EngineFieldProps>;
   title?: string;
   description?: ReactNode;
   placeholder?: unknown;
diff --git a/frontend/src/metabase/databases/utils/engine.ts b/frontend/src/metabase/databases/utils/engine.ts
index 39ba32f61e2..af754e4b4c1 100644
--- a/frontend/src/metabase/databases/utils/engine.ts
+++ b/frontend/src/metabase/databases/utils/engine.ts
@@ -30,3 +30,7 @@ export const getEngineLogo = (engine: string): string | undefined => {
   const logo = ENGINE_LOGO[engine];
   return logo ? `app/assets/img/drivers/${logo}` : undefined;
 };
+
+export const getDefaultEngineKey = (engines: Record<string, Engine>) => {
+  return engines["postgres"] ? "postgres" : Object.keys(engines)[0];
+};
diff --git a/frontend/src/metabase/databases/utils/schema.ts b/frontend/src/metabase/databases/utils/schema.ts
index f31f7d31684..d3c28f8706f 100644
--- a/frontend/src/metabase/databases/utils/schema.ts
+++ b/frontend/src/metabase/databases/utils/schema.ts
@@ -23,6 +23,9 @@ export const getValidationSchema = (
     }),
     auto_run_queries: Yup.boolean().default(true),
     refingerprint: Yup.boolean().default(false),
+    is_sample: Yup.boolean().default(false),
+    is_full_sync: Yup.boolean().default(true),
+    is_on_demand: Yup.boolean().default(false),
   });
 };
 
-- 
GitLab