Repository: FarmBot/Farmbot-Web-App (ts)

Issue:
Dropdowns and Input fields should be consistently styled throughout the app
- [ ] Color of text should be #434343
- [ ] Padding and overall height should be consistent at 30px
- [ ] Text should be vertically aligned in the field

Example of inconsistent fields:
<img width="893" alt="Screen_Shot_2019-07-03_at_11 41 26_PM" src="https://user-images.githubusercontent.com/5524043/61662420-4338cd00-ac83-11e9-84d9-2893c52c3453.png">


Pull request:
Add plant depth field
fixes #1301

Patch:
diff --git a/app/models/celery_script_settings_bag.rb b/app/models/celery_script_settings_bag.rb
index d1a122dd5..256b7c3c9 100644
--- a/app/models/celery_script_settings_bag.rb
+++ b/app/models/celery_script_settings_bag.rb
@@ -184,6 +184,7 @@ module CeleryScriptSettingsBag
     pointer_type: { defn: [e(:PointType)] },
     priority: { defn: [v(:integer)] },
     radius: { defn: [v(:integer)] },
+    depth: { defn: [v(:integer)] },
     resource_id: { defn: [v(:integer)] },
     resource_type: { defn: [e(:resource_type)] },
     resource: { defn: [n(:identifier), n(:resource), n(:point)] },
diff --git a/app/models/plant.rb b/app/models/plant.rb
index 8d1f54582..a330f1d95 100644
--- a/app/models/plant.rb
+++ b/app/models/plant.rb
@@ -8,6 +8,7 @@ class Plant < Point
              planted_at
              pointer_type
              radius
+             depth
              x
              y
              z)
diff --git a/app/mutations/devices/seeders/demo_account_seeder.rb b/app/mutations/devices/seeders/demo_account_seeder.rb
index 480a17f17..d28fbe828 100644
--- a/app/mutations/devices/seeders/demo_account_seeder.rb
+++ b/app/mutations/devices/seeders/demo_account_seeder.rb
@@ -77,23 +77,6 @@ module Devices
         DEMO_LOGS
           .map { |p| p.merge(device: device) }
           .map { |p| Logs::Create.run!(p) }
-        25.times do |n|
-          up = n * 60 * 60
-          p = {
-            device: device,
-            soc_temp: 40,
-            throttled: "0x0",
-            wifi_level_percent: 80 + rand(-8...8),
-            uptime: up,
-            memory_usage: 50 + rand(-5...5),
-            disk_usage: 1,
-            cpu_usage: 5 + rand(-4...4),
-            target: "demo",
-            fbos_version: "100.0.0",
-          }
-          record = Telemetries::Create.run!(p)
-          record.update!(created_at: Time.now - 1.days + up.seconds)
-        end
         device
           .update!(fbos_version: READ_COMMENT_ABOVE)
         device
diff --git a/app/mutations/devices/seeders/plant_fixtures.yml b/app/mutations/devices/seeders/plant_fixtures.yml
index bfe6191cf..89cfe40b1 100644
--- a/app/mutations/devices/seeders/plant_fixtures.yml
+++ b/app/mutations/devices/seeders/plant_fixtures.yml
@@ -7,6 +7,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 400.0
   y: 100.0
   z: 0.0
@@ -18,6 +19,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 400.0
   y: 300.0
   z: 0.0
@@ -29,6 +31,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 600.0
   y: 100.0
   z: 0.0
@@ -40,6 +43,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 600.0
   y: 300.0
   z: 0.0
@@ -51,6 +55,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 800.0
   y: 300.0
   z: 0.0
@@ -62,6 +67,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 800.0
   y: 100.0
   z: 0.0
@@ -73,6 +79,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 600.0
   y: 700.0
   z: 0.0
@@ -84,6 +91,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 800.0
   y: 1100.0
   z: 0.0
@@ -95,6 +103,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 600.0
   y: 1100.0
   z: 0.0
@@ -106,6 +115,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 400.0
   y: 1100.0
   z: 0.0
@@ -117,6 +127,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 200.0
   y: 1100.0
   z: 0.0
diff --git a/app/mutations/devices/seeders/tool_slot_fixtures.yml b/app/mutations/devices/seeders/tool_slot_fixtures.yml
index 340ecdce3..e815c49c6 100644
--- a/app/mutations/devices/seeders/tool_slot_fixtures.yml
+++ b/app/mutations/devices/seeders/tool_slot_fixtures.yml
@@ -7,6 +7,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 100.0
   z: -200.0
@@ -18,6 +19,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 500.0
   z: -200.0
@@ -29,6 +31,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 200.0
   z: -200.0
@@ -40,6 +43,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 300.0
   z: -200.0
@@ -51,6 +55,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 600.0
   z: -200.0
@@ -62,6 +67,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 0.0
   y: 25.0
   z: -200.0
@@ -73,6 +79,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 0.0
   y: 50.0
   z: -200.0
@@ -84,6 +91,7 @@
   planted_at:
   pullout_direction: 1
   radius: 25.0
+  depth: 0
   x: 50.0
   y: 700.0
   z: -200.0
@@ -95,6 +103,7 @@
   planted_at:
   pullout_direction: 0
   radius: 25.0
+  depth: 0
   x: 0.0
   y: 75.0
   z: -200.0
diff --git a/app/mutations/points/create.rb b/app/mutations/points/create.rb
index 50c46c2d1..5a922adef 100644
--- a/app/mutations/points/create.rb
+++ b/app/mutations/points/create.rb
@@ -45,6 +45,7 @@ module Points
     optional do
       hstore :meta
       float :radius, default: 25
+      integer :depth, default: 0
       integer :pullout_direction,
               min: ToolSlot::PULLOUT_DIRECTIONS.min,
               max: ToolSlot::PULLOUT_DIRECTIONS.max
diff --git a/app/mutations/points/query.rb b/app/mutations/points/query.rb
index 2e7ab5161..7d1b552f5 100644
--- a/app/mutations/points/query.rb
+++ b/app/mutations/points/query.rb
@@ -10,6 +10,7 @@ module Points
 
     optional do
       float :radius
+      integer :depth
       float :x
       float :y
       float :z
diff --git a/app/mutations/points/update.rb b/app/mutations/points/update.rb
index 8173e6953..ca3bbb5bb 100644
--- a/app/mutations/points/update.rb
+++ b/app/mutations/points/update.rb
@@ -13,6 +13,7 @@ module Points
       float :y
       float :z
       float :radius
+      integer :depth
       string :name
       string :openfarm_slug
       integer :pullout_direction, in: ToolSlot::PULLOUT_DIRECTIONS
diff --git a/app/serializers/plant_serializer.rb b/app/serializers/plant_serializer.rb
index 40cf71326..a659436f7 100644
--- a/app/serializers/plant_serializer.rb
+++ b/app/serializers/plant_serializer.rb
@@ -1,5 +1,5 @@
 class PlantSerializer < BasePointSerializer
-  attributes :openfarm_slug, :plant_stage, :planted_at, :radius, :meta
+  attributes :openfarm_slug, :plant_stage, :planted_at, :radius, :depth, :meta
 
   def x
     object.x.round
diff --git a/db/migrate/20221027211207_add_depth_to_points.rb b/db/migrate/20221027211207_add_depth_to_points.rb
new file mode 100644
index 000000000..20a3f7231
--- /dev/null
+++ b/db/migrate/20221027211207_add_depth_to_points.rb
@@ -0,0 +1,9 @@
+class AddDepthToPoints < ActiveRecord::Migration[6.1]
+  def up
+    add_column :points, :depth, :integer, default: 0
+  end
+
+  def down
+    remove_column :points, :depth
+  end
+end
diff --git a/db/structure.sql b/db/structure.sql
index fc7c5914d..5605d6b0d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -869,7 +869,8 @@ CREATE TABLE public.points (
     tool_id integer,
     pullout_direction integer DEFAULT 0,
     discarded_at timestamp without time zone,
-    gantry_mounted boolean DEFAULT false
+    gantry_mounted boolean DEFAULT false,
+    depth integer DEFAULT 0
 );
 
 
@@ -3831,6 +3832,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20220415191331'),
 ('20220620225957'),
 ('20220810212545'),
-('20220819170955');
+('20220819170955'),
+('20221027211207');
 
 
diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss
index b228aa7cb..360ec9e50 100644
--- a/frontend/css/farm_designer/farm_designer_panels.scss
+++ b/frontend/css/farm_designer/farm_designer_panels.scss
@@ -429,10 +429,10 @@
       height: 23rem;
     }
     &.more-action {
-      height: 32rem;
+      height: 35rem;
     }
     &.more-select.more-action {
-      height: 40rem;
+      height: 43rem;
     }
     button {
       margin: 0.5rem;
@@ -453,6 +453,7 @@
       padding-right: 1rem;
     }
     .point-size-bulk-update,
+    .plant-depth-bulk-update,
     .plant-date-bulk-update,
     .point-color-bulk-update,
     .plant-slug-bulk-update,
@@ -472,6 +473,7 @@
         vertical-align: top;
       }
     }
+    .plant-depth-bulk-update,
     .point-size-bulk-update {
       p {
         display: inline-block;
@@ -485,6 +487,12 @@
         margin-left: 0.5rem;
       }
     }
+    .plant-depth-bulk-update {
+      input {
+        margin-top: 1rem;
+        margin-left: 0.8rem;
+      }
+    }
     .plant-date-bulk-update {
       .input {
         display: inline-block;
@@ -555,10 +563,10 @@
       padding-top: 23rem;
     }
     &.more-action {
-      padding-top: 32rem;
+      padding-top: 35rem;
     }
     &.more-select.more-action {
-      padding-top: 40rem;
+      padding-top: 43rem;
     }
     .tool-slot-search-item-icon {
       padding-left: 5px !important;
@@ -973,6 +981,11 @@ li {
       display: inline-block;
       margin-left: 0.5rem;
     }
+    .filter-search {
+      .bp4-popover-wrapper {
+        margin-left: 0;
+      }
+    }
   }
   input {
     background: $white;
@@ -986,6 +999,11 @@ li {
   .delete {
     height: 2rem;
   }
+  .garden-indicator {
+    display: inline;
+    margin-left: 2rem;
+    font-size: 1rem;
+  }
 }
 
 .section-header {
@@ -1613,7 +1631,7 @@ li {
   .change-password {
     .old-password,
     .new-password {
-      margin-bottom: 1rem;
+      margin-top: 1.5rem;
       input {
         width: 50%;
         float: right;
@@ -1629,6 +1647,7 @@ li {
     }
   }
   .release-notes-wrapper {
+    margin-top: 0.75rem;
     float: right !important;
   }
   .settings-warning-banner {
diff --git a/frontend/css/inputs.scss b/frontend/css/inputs.scss
index a8fe23e9e..3fd7e5750 100644
--- a/frontend/css/inputs.scss
+++ b/frontend/css/inputs.scss
@@ -44,6 +44,10 @@ input:not([role="combobox"]) {
   }
 }
 
+input:not([role="combobox"]):not([type="checkbox"]):not([type="radio"]) {
+  height: 3rem;
+}
+
 .input {
   position: relative;
   .bp4-popover-wrapper {
diff --git a/frontend/devices/connectivity/fbos_metric_demo_data.ts b/frontend/devices/connectivity/fbos_metric_demo_data.ts
new file mode 100644
index 000000000..1c139b5dd
--- /dev/null
+++ b/frontend/devices/connectivity/fbos_metric_demo_data.ts
@@ -0,0 +1,30 @@
+import { SpecialStatus, TaggedTelemetry } from "farmbot";
+import { random, range } from "lodash";
+
+export const generateDemoTelemetry = (): TaggedTelemetry[] => {
+  const count = 100;
+  return range(count).map(n => {
+    const up = n / count * 24 * 60 * 60;
+    const at = (new Date()).valueOf() / 1000 - 24 * 60 * 60 + up;
+    return {
+      kind: "Telemetry",
+      uuid: `Telemetry.${n}.${n}`,
+      specialStatus: SpecialStatus.SAVED,
+      body: {
+        id: n + 1,
+        soc_temp: 40,
+        throttled: "0x0",
+        wifi_level_percent: 80 + random(-8, 8),
+        uptime: up,
+        memory_usage: 50 + random(-5, 5),
+        disk_usage: 1,
+        cpu_usage: 5 + random(-4, 4),
+        target: "demo",
+        fbos_version: "100.0.0",
+        firmware_hardware: "farmduino_k16",
+        created_at: at,
+        updated_at: (new Date(at)).toISOString(),
+      }
+    };
+  });
+};
diff --git a/frontend/devices/connectivity/fbos_metric_history_table.tsx b/frontend/devices/connectivity/fbos_metric_history_table.tsx
index deea61421..279e01dfa 100644
--- a/frontend/devices/connectivity/fbos_metric_history_table.tsx
+++ b/frontend/devices/connectivity/fbos_metric_history_table.tsx
@@ -12,6 +12,8 @@ import { formatTime } from "../../util";
 import moment from "moment";
 import { Telemetry } from "farmbot/dist/resources/api_resources";
 import { cloneDeep, sortBy } from "lodash";
+import { forceOnline } from "../must_be_online";
+import { generateDemoTelemetry } from "./fbos_metric_demo_data";
 
 const METRIC_TITLES = (): Partial<Record<keyof Telemetry, string>> => ({
   soc_temp: t("Temp"),
@@ -86,6 +88,7 @@ export interface FbosMetricHistoryTableProps {
 interface FbosMetricHistoryState {
   hoveredMetric: keyof Telemetry | undefined;
   hoveredTime: number | undefined;
+  demoData: TaggedTelemetry[],
 }
 
 export class FbosMetricHistoryTable
@@ -93,6 +96,7 @@ export class FbosMetricHistoryTable
   state: FbosMetricHistoryState = {
     hoveredMetric: undefined,
     hoveredTime: undefined,
+    demoData: generateDemoTelemetry(),
   };
 
   hoverMetric: OnMetricHover = metricName => () =>
@@ -101,6 +105,10 @@ export class FbosMetricHistoryTable
   hoverTime = (time: number | undefined) => () =>
     this.setState({ hoveredTime: time });
 
+  get telemetry() {
+    return forceOnline() ? this.state.demoData : this.props.telemetry;
+  }
+
   render() {
     const commonProps = {
       hoveredMetric: this.state.hoveredMetric,
@@ -109,7 +117,7 @@ export class FbosMetricHistoryTable
     };
     const rightAlignProps = { ...commonProps, rightAlign: true };
     return <div className={"fbos-metric-history"}>
-      <FbosMetricHistoryPlot {...commonProps} telemetry={this.props.telemetry} />
+      <FbosMetricHistoryPlot {...commonProps} telemetry={this.telemetry} />
       <div className={"fbos-metric-history-table-wrapper"}>
         <table>
           <thead>
@@ -128,7 +136,7 @@ export class FbosMetricHistoryTable
             </tr>
           </thead>
           <tbody>
-            {sortBy(cloneDeep(this.props.telemetry), "body.created_at").reverse()
+            {sortBy(cloneDeep(this.telemetry), "body.created_at").reverse()
               .map(m => {
                 const recordSelected = this.state.hoveredTime == m.body.created_at;
                 const recordProps = {
diff --git a/frontend/farm_designer/map/profile/plants_and_weeds.tsx b/frontend/farm_designer/map/profile/plants_and_weeds.tsx
index 3d4010a9f..f7d4e47f7 100644
--- a/frontend/farm_designer/map/profile/plants_and_weeds.tsx
+++ b/frontend/farm_designer/map/profile/plants_and_weeds.tsx
@@ -8,6 +8,7 @@ import { TaggedPlant } from "../interfaces";
 import { cachedCrop } from "../../../open_farm/cached_crop";
 import { BooleanSetting } from "../../../session_keys";
 import { FilePath } from "../../../internal_urls";
+import { PlantPointer } from "farmbot/dist/resources/api_resources";
 
 /** Plant point profile. */
 export class PlantPoint
@@ -31,6 +32,9 @@ export class PlantPoint
     const spreadRadius = (spreadDiaCm || defaultSpreadCmDia(radius)) / 2 * 10;
     const profileX = getX(point.body);
     const profileY = point.body.z == 0 ? soilHeight : point.body.z;
+    const depth = point.kind == "Point"
+      ? point.body["depth" as keyof PlantPointer] as number
+      : 0;
     return <g id={"plant-profile-point"}>
       <defs>
         <radialGradient id={"plant-radius-gradient"}>
@@ -72,6 +76,12 @@ export class PlantPoint
           opacity={0.5} fill={"none"} />}
       <circle id={"point-coordinate-indicator"} opacity={0.5}
         fill={Color.darkGreen} cx={profileX} cy={profileY} r={5} />
+      <line id={"point-depth-indicator"} opacity={0.5}
+        stroke={Color.darkGreen} strokeWidth={2}
+        x1={profileX} y1={profileY}
+        x2={profileX} y2={profileY + depth} />
+      <circle id={"point-bottom-indicator"} opacity={0.5}
+        fill={Color.darkGreen} cx={profileX} cy={profileY + depth} r={2} />
     </g>;
   }
 }
diff --git a/frontend/farm_designer/plant.ts b/frontend/farm_designer/plant.ts
index 03b338390..05b925206 100644
--- a/frontend/farm_designer/plant.ts
+++ b/frontend/farm_designer/plant.ts
@@ -16,6 +16,7 @@ export function Plant(options: PlantOptions): PlantPointer {
     y: (options.y || 0),
     z: 0,
     radius: (options.radius || DEFAULT_PLANT_RADIUS),
+    ["depth" as keyof PlantPointer]: 0,
     openfarm_slug,
     plant_stage: "planned"
   };
diff --git a/frontend/plants/edit_plant_status.tsx b/frontend/plants/edit_plant_status.tsx
index e04545907..b65ddbe89 100644
--- a/frontend/plants/edit_plant_status.tsx
+++ b/frontend/plants/edit_plant_status.tsx
@@ -166,7 +166,7 @@ export const PlantDateBulkUpdate = (props: PlantDateBulkUpdateProps) => {
   </div>;
 };
 
-/** Update `radius` for multiple points at once. */
+/** Update `radius` for multiple plants at once. */
 export const PointSizeBulkUpdate = (props: BulkUpdateBaseProps) => {
   const points = props.allPoints.filter(point =>
     props.selected.includes(point.uuid) && point.kind === "Point" &&
@@ -178,6 +178,7 @@ export const PointSizeBulkUpdate = (props: BulkUpdateBaseProps) => {
     <p>{t("update radius to")}</p>
     <input
       value={radius}
+      min={0}
       onChange={e => setRadius(e.currentTarget.value)}
       onBlur={() => {
         const radiusNum = parseInt(radius);
@@ -192,6 +193,36 @@ export const PointSizeBulkUpdate = (props: BulkUpdateBaseProps) => {
   </div>;
 };
 
+/** Update `depth` for multiple points at once. */
+export const PlantDepthBulkUpdate = (props: BulkUpdateBaseProps) => {
+  const points = props.allPoints.filter(point =>
+    props.selected.includes(point.uuid) && point.kind === "Point" &&
+    point.body.pointer_type == "Plant")
+    .map((p: TaggedPlantPointer) => p);
+  const averageDepth = round(mean(points.map(p =>
+    p.body["depth" as keyof TaggedPlantPointer["body"]])));
+  const [depth, setDepth] = React.useState("" + (averageDepth || 0));
+  return <div className={"plant-depth-bulk-update"}>
+    <p>{t("update depth to")}</p>
+    <input
+      value={depth}
+      min={0}
+      onChange={e => setDepth(e.currentTarget.value)}
+      onBlur={() => {
+        const depthNum = parseFloat(depth);
+        isFinite(depthNum) && points.length > 0 && confirm(
+          t("Change depth to {{ depth }}mm for {{ num }} items?",
+            { depth: depthNum, num: points.length }))
+          && points.map(point => {
+            props.dispatch(edit(point, {
+              ["depth" as keyof TaggedPlantPointer["body"]]: depthNum,
+            }));
+            props.dispatch(save(point.uuid));
+          });
+      }} />
+  </div>;
+};
+
 /** Update `meta.color` for multiple points at once. */
 export const PointColorBulkUpdate = (props: BulkUpdateBaseProps) => {
   const points = props.allPoints.filter(point =>
diff --git a/frontend/plants/map_state_to_props.ts b/frontend/plants/map_state_to_props.ts
index 6fc5b402d..18777cd68 100644
--- a/frontend/plants/map_state_to_props.ts
+++ b/frontend/plants/map_state_to_props.ts
@@ -56,6 +56,7 @@ export interface FormattedPlantInfo {
   y: number;
   z: number;
   radius: number;
+  depth: number;
   id: number | undefined;
   name: string;
   uuid: string;
@@ -90,6 +91,7 @@ export function formatPlantInfo(plant: TaggedPlant): FormattedPlantInfo {
     y: plant.body.y,
     z: plant.body.z,
     radius: plant.body.radius,
+    depth: plant.body["depth" as keyof TaggedPlant["body"]] as number,
     uuid: plant.uuid,
     plantedAt: plantDate(plant),
     plantStatus: get(plant, "body.plant_stage", "planned"),
diff --git a/frontend/plants/plant_inventory.tsx b/frontend/plants/plant_inventory.tsx
index 61e2491d4..28f9eafe3 100644
--- a/frontend/plants/plant_inventory.tsx
+++ b/frontend/plants/plant_inventory.tsx
@@ -173,6 +173,8 @@ export class RawPlants
           addTitle={t("add plant")}
           addClassName={"plus-plant"}
           title={t("Plants")}
+          extraHeaderTitle={!!this.props.openedSavedGarden &&
+            <i className={"garden-indicator"}>{t("saved garden")}</i>}
           extraHeaderContent={
             !this.props.openedSavedGarden && plantsPanelState.plants &&
             <button className={"fb-button red delete"}
@@ -223,6 +225,7 @@ export interface PanelSectionProps {
   addClassName: string;
   children: JSX.Element | JSX.Element[];
   extraHeaderContent?: JSX.Element | false;
+  extraHeaderTitle?: JSX.Element | false;
 }
 
 export const PanelSection = (props: PanelSectionProps) => {
@@ -231,6 +234,7 @@ export const PanelSection = (props: PanelSectionProps) => {
     <div className={"section-header"}
       onClick={props.toggleOpen}>
       <label>{`${props.title} (${props.itemCount})`}</label>
+      {props.extraHeaderTitle}
       <i className={`fa fa-caret-${isOpen ? "up" : "down"}`} />
       {isOpen && <div
         onClick={e => {
diff --git a/frontend/plants/plant_panel.tsx b/frontend/plants/plant_panel.tsx
index 5b71d9a06..e360e29fd 100644
--- a/frontend/plants/plant_panel.tsx
+++ b/frontend/plants/plant_panel.tsx
@@ -20,6 +20,7 @@ import { Actions } from "../constants";
 import { daysOldText } from "./plant_inventory_item";
 import { GoToThisLocationButton } from "../farm_designer/move_to";
 import { BotPosition } from "../devices/interfaces";
+import { PlantPointer } from "farmbot/dist/resources/api_resources";
 
 export interface PlantPanelProps {
   info: FormattedPlantInfo;
@@ -109,6 +110,25 @@ export const EditPlantRadius = (props: EditPlantRadiusProps) =>
     </Col>
   </Row>;
 
+export interface EditPlantDepthProps extends EditPlantProperty {
+  depth: number;
+}
+
+export const EditPlantDepth = (props: EditPlantDepthProps) =>
+  <Row>
+    <Col xs={6}>
+      <label style={{ marginTop: 0 }}>{t("depth (mm)")}</label>
+      <BlurableInput
+        type="number"
+        name="depth"
+        value={props.depth}
+        min={0}
+        onCommit={e => props.updatePlant(props.uuid, {
+          ["depth" as keyof PlantPointer]: parseIntInput(e.currentTarget.value)
+        })} />
+    </Col>
+  </Row>;
+
 interface DeleteButtonsProps {
   destroy(): void;
 }
@@ -206,6 +226,9 @@ export function PlantPanel(props: PlantPanelProps) {
       <ListItem>
         <EditPlantRadius {...commonProps} radius={info.radius} />
       </ListItem>
+      <ListItem>
+        <EditPlantDepth {...commonProps} depth={info.depth} />
+      </ListItem>
       <ListItem name={t("Status")}>
         {(!inSavedGarden)
           ? <EditPlantStatus {...commonProps} plantStatus={plantStatus} />
diff --git a/frontend/plants/select_plants.tsx b/frontend/plants/select_plants.tsx
index 6332af4f8..d851426c7 100644
--- a/frontend/plants/select_plants.tsx
+++ b/frontend/plants/select_plants.tsx
@@ -21,6 +21,7 @@ import {
   PlantDateBulkUpdate, PlantSlugBulkUpdate, PlantStatusBulkUpdate,
   PointColorBulkUpdate,
   PointSizeBulkUpdate,
+  PlantDepthBulkUpdate,
 } from "./edit_plant_status";
 import { FBSelect, DropDownItem } from "../ui";
 import {
@@ -302,6 +303,12 @@ export class RawSelectPlants
               allPoints={this.props.allPoints}
               selected={this.selected}
               dispatch={this.props.dispatch} />}
+          {["Plant"]
+            .includes(this.selectionPointType) &&
+            <PlantDepthBulkUpdate
+              allPoints={this.props.allPoints}
+              selected={this.selected}
+              dispatch={this.props.dispatch} />}
           {["Weed", "GenericPointer"]
             .includes(this.selectionPointType) &&
             <PointColorBulkUpdate
diff --git a/spec/factories/plants.rb b/spec/factories/plants.rb
index 543c5a8a3..cde54fab3 100644
--- a/spec/factories/plants.rb
+++ b/spec/factories/plants.rb
@@ -1,6 +1,7 @@
 FactoryBot.define do
   factory :plant do
     radius { 1.5 }
+    depth { 0 }
     x { rand(1...550) }
     y { rand(1...550) }
     z { rand(1...550) }


Test patch:
diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts
index 0928cff3a..c2ec76cbc 100644
--- a/frontend/__test_support__/fake_state/resources.ts
+++ b/frontend/__test_support__/fake_state/resources.ts
@@ -159,6 +159,7 @@ export function fakePlant(): TaggedPlantPointer {
     y: 200,
     z: 0,
     radius: 25,
+    ["depth" as keyof TaggedPlantPointer["body"]]: 0,
     meta: {},
     openfarm_slug: "strawberry"
   });
@@ -173,6 +174,7 @@ export function fakePoint(): TaggedGenericPointer {
     y: 400,
     z: 0,
     radius: 100,
+    ["depth" as keyof TaggedPlantPointer["body"]]: 0,
     meta: { created_by: "plant-detection" }
   });
 }
@@ -186,6 +188,7 @@ export function fakeWeed(): TaggedWeedPointer {
     y: 400,
     z: 0,
     radius: 100,
+    ["depth" as keyof TaggedPlantPointer["body"]]: 0,
     plant_stage: "active",
     meta: { created_by: "plant-detection", color: "red" }
   });
@@ -203,6 +206,7 @@ export function fakePlantTemplate(): TaggedPlantTemplate {
     id: idCounter++,
     saved_garden_id: idCounter++,
     radius: 50,
+    ["depth" as keyof TaggedPlantPointer["body"]]: 0,
     x: 100,
     y: 200,
     z: 0,
diff --git a/frontend/__test_support__/resource_index_builder.ts b/frontend/__test_support__/resource_index_builder.ts
index 8c039e037..6c26474f6 100644
--- a/frontend/__test_support__/resource_index_builder.ts
+++ b/frontend/__test_support__/resource_index_builder.ts
@@ -185,6 +185,7 @@ const tr7: TaggedPoint = {
     "name": "fenestrate-flower-3632",
     "pointer_type": "Plant",
     "radius": 46,
+    ["depth" as keyof TaggedPoint["body"]]: 0,
     "x": 347,
     "y": 385,
     "z": 0,
@@ -207,6 +208,7 @@ const tr8: TaggedPoint = {
     "name": "alate-fire-7363",
     "pointer_type": "Plant",
     "radius": 36,
+    ["depth" as keyof TaggedPoint["body"]]: 0,
     "x": 727,
     "y": 376,
     "z": 0,
@@ -230,6 +232,7 @@ const tr9: TaggedPoint = {
     "name": "untitled",
     "pointer_type": "GenericPointer",
     "radius": 6,
+    ["depth" as keyof TaggedPoint["body"]]: 0,
     "x": 1245,
     "y": 637,
     "z": 5
@@ -251,6 +254,7 @@ const tr10: TaggedPoint = {
     "name": "untitled",
     "pointer_type": "GenericPointer",
     "radius": 10,
+    ["depth" as keyof TaggedPoint["body"]]: 0,
     "x": 490,
     "y": 421,
     "z": 5
@@ -335,6 +339,7 @@ const tr16: TaggedPoint = {
     pointer_type: "Weed",
     plant_stage: "active",
     radius: 10,
+    ["depth" as keyof TaggedPoint["body"]]: 0,
     x: 490,
     y: 421,
     z: 5
diff --git a/frontend/devices/connectivity/__tests__/fbos_metric_history_table_test.tsx b/frontend/devices/connectivity/__tests__/fbos_metric_history_table_test.tsx
index ee3cf801a..df3f10c38 100644
--- a/frontend/devices/connectivity/__tests__/fbos_metric_history_table_test.tsx
+++ b/frontend/devices/connectivity/__tests__/fbos_metric_history_table_test.tsx
@@ -2,6 +2,11 @@ jest.mock("../fbos_metric_history_plot", () => ({
   FbosMetricHistoryPlot: () => <div />,
 }));
 
+let mockDemo = false;
+jest.mock("../../must_be_online", () => ({
+  forceOnline: () => mockDemo,
+}));
+
 import React from "react";
 import { mount } from "enzyme";
 import { fakeTelemetry } from "../../../__test_support__/fake_state/resources";
@@ -27,8 +32,20 @@ describe("<FbosMetricHistoryTable />", () => {
 
   it("renders", () => {
     const p = fakeProps();
-    const wrapper = mount(<FbosMetricHistoryTable {...p} />);
+    const wrapper = mount<FbosMetricHistoryTable>(
+      <FbosMetricHistoryTable {...p} />);
+    expect(wrapper.instance().telemetry.length).toEqual(3);
+    expect(wrapper.text().toLowerCase()).toContain("wifi");
+  });
+
+  it("renders demo data", () => {
+    mockDemo = true;
+    const p = fakeProps();
+    const wrapper = mount<FbosMetricHistoryTable>(
+      <FbosMetricHistoryTable {...p} />);
+    expect(wrapper.instance().telemetry.length).toEqual(100);
     expect(wrapper.text().toLowerCase()).toContain("wifi");
+    mockDemo = false;
   });
 
   it("sets metric hover state", () => {
diff --git a/frontend/farm_designer/__tests__/plant_test.ts b/frontend/farm_designer/__tests__/plant_test.ts
index 12bb2eac5..b94c7c1cd 100644
--- a/frontend/farm_designer/__tests__/plant_test.ts
+++ b/frontend/farm_designer/__tests__/plant_test.ts
@@ -11,6 +11,7 @@ describe("Plant()", () => {
       plant_stage: "planned",
       pointer_type: "Plant",
       radius: 25,
+      depth: 0,
       x: 0,
       y: 0,
       z: 0,
diff --git a/frontend/farm_designer/map/profile/__tests__/plants_and_weeds_test.tsx b/frontend/farm_designer/map/profile/__tests__/plants_and_weeds_test.tsx
index ae302d1ea..4004c86bf 100644
--- a/frontend/farm_designer/map/profile/__tests__/plants_and_weeds_test.tsx
+++ b/frontend/farm_designer/map/profile/__tests__/plants_and_weeds_test.tsx
@@ -6,7 +6,7 @@ jest.mock("../../../../open_farm/cached_crop", () => ({
 import React from "react";
 import { svgMount } from "../../../../__test_support__/svg_mount";
 import {
-  fakePlant, fakeWeed,
+  fakePlant, fakePlantTemplate, fakeWeed,
 } from "../../../../__test_support__/fake_state/resources";
 import { TaggedWeedPointer } from "farmbot";
 import { TaggedPlant } from "../../interfaces";
@@ -44,6 +44,13 @@ describe("<PlantPoint />", () => {
     expect(wrapper.find("#point-coordinate-indicator").props().cy).toEqual(100);
   });
 
+  it("renders plant template", () => {
+    const p = fakeProps();
+    p.point = fakePlantTemplate();
+    const wrapper = svgMount(<PlantPoint {...p} />);
+    expect(wrapper.find("#plant-profile-point").length).toEqual(1);
+  });
+
   it("renders default spread", () => {
     mockSpread = 0;
     const wrapper = svgMount(<PlantPoint {...fakeProps()} />);
diff --git a/frontend/plants/__tests__/edit_plant_status_test.tsx b/frontend/plants/__tests__/edit_plant_status_test.tsx
index 2af97f969..af8b695ae 100644
--- a/frontend/plants/__tests__/edit_plant_status_test.tsx
+++ b/frontend/plants/__tests__/edit_plant_status_test.tsx
@@ -19,6 +19,7 @@ import {
   PlantDateBulkUpdate,
   PlantSlugBulkUpdate,
   PlantSlugBulkUpdateProps,
+  PlantDepthBulkUpdate,
 } from "../edit_plant_status";
 import { fakeTimeSettings } from "../../__test_support__/fake_time_settings";
 import { Actions } from "../../constants";
@@ -199,6 +200,24 @@ describe("<PointSizeBulkUpdate />", () => {
     expect(edit).toHaveBeenCalledWith(plant1, { radius: 1 });
     expect(edit).toHaveBeenCalledWith(plant2, { radius: 1 });
   });
+
+  it("updates plant depths", () => {
+    const p = fakeProps();
+    const plant1 = fakePlant();
+    const plant2 = fakePlant();
+    const plant3 = fakePlant();
+    p.allPoints = [plant1, plant2, plant3];
+    p.selected = [plant1.uuid, plant2.uuid];
+    const wrapper = shallow(<PlantDepthBulkUpdate {...p} />);
+    window.confirm = jest.fn(() => true);
+    wrapper.find("input").simulate("change", { currentTarget: { value: "1" } });
+    wrapper.find("input").simulate("blur");
+    expect(window.confirm).toHaveBeenCalledWith(
+      "Change depth to 1mm for 2 items?");
+    expect(edit).toHaveBeenCalledTimes(2);
+    expect(edit).toHaveBeenCalledWith(plant1, { depth: 1 });
+    expect(edit).toHaveBeenCalledWith(plant2, { depth: 1 });
+  });
 });
 
 describe("<PointColorBulkUpdate />", () => {
diff --git a/frontend/plants/__tests__/plant_panel_test.tsx b/frontend/plants/__tests__/plant_panel_test.tsx
index fa90ce15b..991ab55b5 100644
--- a/frontend/plants/__tests__/plant_panel_test.tsx
+++ b/frontend/plants/__tests__/plant_panel_test.tsx
@@ -11,6 +11,8 @@ import {
   EditPlantLocation,
   EditPlantRadiusProps,
   EditPlantRadius,
+  EditPlantDepthProps,
+  EditPlantDepth,
 } from "../plant_panel";
 import { shallow, mount } from "enzyme";
 import { FormattedPlantInfo } from "../map_state_to_props";
@@ -31,6 +33,7 @@ describe("<PlantPanel/>", () => {
     y: 34,
     z: 0,
     radius: 25,
+    depth: 0,
     id: undefined,
     name: "tomato",
     uuid: "Plant.0.0",
@@ -189,3 +192,22 @@ describe("<EditPlantRadius />", () => {
     });
   });
 });
+
+describe("<EditPlantDepth />", () => {
+  const fakeProps = (): EditPlantDepthProps => ({
+    uuid: "Plant.0.0",
+    depth: 10,
+    updatePlant: jest.fn(),
+  });
+
+  it("changes location", () => {
+    const p = fakeProps();
+    const wrapper = shallow(<EditPlantDepth {...p} />);
+    wrapper.find("BlurableInput").first().simulate("commit", {
+      currentTarget: { value: "100" }
+    });
+    expect(p.updatePlant).toHaveBeenCalledWith("Plant.0.0", {
+      depth: 100
+    });
+  });
+});