diff --git a/Procfile b/Procfile
index 76f9d192d..9a78e97e1 100644
--- a/Procfile
+++ b/Procfile
@@ -1,5 +1,5 @@
worker: bundle exec rake jobs:work
rabbit_workers: bin/rails r lib/rabbit_workers.rb
-web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size $MAX_POOL_SIZE
+web: bundle exec passenger start -p $PORT -e $RAILS_ENV --max-pool-size ${MAX_POOL_SIZE:-1} --max-requests ${MAX_REQUESTS:-1000}
# This will perform a hard refresh on all connected browsers.
release: rails r "User.refresh_everyones_ui" && rails db:migrate && (bundle exec rake hook:release_info || true)
diff --git a/app/models/device.rb b/app/models/device.rb
index a7c0873b3..542da2249 100644
--- a/app/models/device.rb
+++ b/app/models/device.rb
@@ -124,15 +124,19 @@ def trim_excess_telemetry
# Give the user back the amount of sensor readings they are allowed to view.
def limited_sensor_readings_list
sensor_readings
- .order(created_at: :desc)
+ .order(created_at: :desc, id: :desc)
.limit(DEFAULT_MAX_SENSOR_READINGS)
end
def excess_sensor_readings
+ sensor_readings.where(id: excess_sensor_reading_ids_subquery)
+ end
+
+ def excess_sensor_reading_ids_subquery
sensor_readings
- .where
- .not(id: limited_sensor_readings_list.pluck(:id))
- .where(device_id: self.id)
+ .order(created_at: :desc, id: :desc)
+ .offset(DEFAULT_MAX_SENSOR_READINGS)
+ .select(:id)
end
def trim_excess_sensor_readings
diff --git a/docker-compose.yml b/docker-compose.yml
index e84faf8b8..19e018111 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -50,7 +50,7 @@ services:
build:
context: "."
dockerfile: docker_configs/api.Dockerfile
- command: bash -c "rm -f tmp/pids/server.pid && bundle exec passenger start"
+ command: bash -c "rm -f tmp/pids/server.pid && bundle exec passenger start --max-pool-size ${MAX_POOL_SIZE:-1} --max-requests ${MAX_REQUESTS:-1000}"
ports: ["${API_PORT}:${API_PORT}"]
mqtt:
diff --git a/example.env b/example.env
index a42c94a50..995dca681 100644
--- a/example.env
+++ b/example.env
@@ -142,7 +142,12 @@ CODECOV_TOKEN=
# Set the max pool size for Passenger. (Only needed if using Heroku)
# FarmBot Inc uses Heroku. Self hosters do not.
-MAX_POOL_SIZE=2
+MAX_POOL_SIZE=1
+
+# Set the max number of requests until restart for Passenger.
+# (Only needed if using Heroku)
+# FarmBot Inc uses Heroku. Self hosters do not.
+MAX_REQUESTS=1000
# This is set by Heroku and used by the frontend to show the current version.
# Most self hosting users will want to delete this.
diff --git a/frontend/css/panels/sequences.scss b/frontend/css/panels/sequences.scss
index f978dad97..12b7be3c0 100644
--- a/frontend/css/panels/sequences.scss
+++ b/frontend/css/panels/sequences.scss
@@ -36,7 +36,8 @@
}
.panel-content {
.sequence-editor-content {
- overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: visible;
@media screen and (max-width: 767px) {
margin-left: 5px;
@@ -44,14 +45,9 @@
}
}
.add-command-button-container {
- display: block;
- height: 0;
- .bp6-collapse {
- padding: 0;
- }
- .bp6-collapse-body {
- padding: 0;
- }
+ align-items: start;
+ justify-content: right;
+ grid-template-columns: 1fr auto;
}
.drag-drop-area {
display: none;
@@ -258,7 +254,6 @@
.sequence-editor-sections {
.bp6-collapse {
- padding: 0 1rem;
.bp6-collapse-body {
padding-bottom: 1rem;
}
@@ -551,8 +546,11 @@
}
}
+.sequence-step-components {
+ padding: 0 1rem;
+}
+
.sequence-steps {
- margin-right: 2.5rem;
.sequence-step {
&.hovered {
box-shadow: 0px 0px 15px #f70;
@@ -571,10 +569,20 @@
}
}
+.sequence-steps > .step-dragger {
+ margin-right: 2.5rem;
+}
+
+.sequence-page {
+ .step-dragger {
+ margin-right: 0;
+ }
+ .add-command-button-container {
+ display: none;
+ }
+}
+
.step-button-cluster {
- overflow-y: auto;
- overflow-x: hidden;
- max-height: calc(100vh - 8rem);
.text-input-wrapper {
input {
padding: 0;
@@ -607,7 +615,6 @@
&.designer-cluster {
margin: 0;
background: $dark_gray;
- margin-bottom: 1rem;
border-radius: 5px;
overflow: unset;
padding: 0.75rem;
@@ -1087,11 +1094,9 @@
}
.add-command-button-container {
- display: none;
- position: absolute;
- z-index: 9;
- width: 100%;
- height: 0;
+ .bp6-collapse {
+ transition-duration: 0ms !important;
+ }
button {
margin: 0;
}
@@ -1103,6 +1108,8 @@
height: 20px;
padding: 0.25rem;
border-radius: 2rem;
+ margin-bottom: -.75rem;
+ right: -2.5rem;
box-shadow: none !important;
background: $dark_gray !important;
i {
@@ -1130,7 +1137,6 @@
&.last {
.bp6-collapse {
margin-top: 1rem;
- margin-right: 2.5rem;
}
.add-command {
margin-top: -2rem;
@@ -1145,10 +1151,6 @@
}
}
&.only {
- position: relative;
- margin: auto;
- text-align: center;
- height: 3rem;
.add-command {
display: none;
}
@@ -1156,18 +1158,8 @@
margin: 0;
}
}
-
- @media screen and (max-width: 767px) {
- display: block;
- .add-command {
- display: block;
- }
- }
&.open {
- position: relative;
.add-command {
- position: absolute;
- right: -2.5rem;
transform: rotate(-45deg);
i {
transform: rotate(0);
diff --git a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx
index 6ff8caae7..0549f72f1 100644
--- a/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx
+++ b/frontend/sequences/__tests__/sequence_editor_middle_active_test.tsx
@@ -8,6 +8,7 @@ import {
SequenceBtnGroup,
SequenceShareMenu,
SequencePublishMenu,
+ AddCommandButton,
isSequencePublished,
AddCommandButtonProps,
} from "../sequence_editor_middle_active";
@@ -835,11 +836,6 @@ describe("", () => {
stepButtonClusterSpy.mockRestore();
});
- const getAddCommandButton = async () => {
- return (await import(`../sequence_editor_middle_active.tsx?m=${Math.random()}`))
- .AddCommandButton;
- };
-
const fakeProps = (): AddCommandButtonProps => ({
dispatch: jest.fn(),
index: 1,
@@ -850,10 +846,9 @@ describe("", () => {
resources: buildResourceIndex().index,
});
- it("dispatches new step position", async () => {
+ it("dispatches new step position", () => {
location.pathname = "";
const p = fakeProps();
- const AddCommandButton = await getAddCommandButton();
const wrapper = createRenderer();
const button = wrapper.root.findAll(node =>
typeof node.props.onClick == "function" &&
@@ -876,12 +871,13 @@ describe("", () => {
unmountRenderer(wrapper);
});
- it("closes cluster", async () => {
- const AddCommandButton = await getAddCommandButton();
+ it("closes cluster", () => {
const { container } = render();
const cluster = container.querySelector(".add-command-button-container");
const close = container.querySelector(".step-button-cluster-close");
if (cluster && close) {
+ expect(cluster.classList.contains("row")).toBeTruthy();
+ expect(cluster.classList.contains("half-gap")).toBeTruthy();
expect(cluster.classList.contains("open")).toBeTruthy();
fireEvent.click(close);
expect(cluster.classList.contains("open")).toBeFalsy();
diff --git a/frontend/sequences/all_steps.tsx b/frontend/sequences/all_steps.tsx
index af0a7fdf2..27aa12043 100644
--- a/frontend/sequences/all_steps.tsx
+++ b/frontend/sequences/all_steps.tsx
@@ -55,15 +55,15 @@ export class AllSteps extends React.Component {
const hovered = this.props.visualized && this.props.hoveredStep === tag
? "hovered"
: "";
- return
+
this.props.onDrop(index, key)} />
{!this.props.readOnly &&
}
- this.props.onDrop(index, key)} />
{
"add-command-button-container",
getPositionClass(),
collapsed ? "" : "open",
+ "row",
+ "half-gap",
].join(" ")}>
+
+ setCollapsed(true)}
+ current={props.sequence}
+ dispatch={dispatch}
+ farmwareData={props.farmwareData}
+ sequences={props.sequences}
+ resources={props.resources}
+ stepIndex={index} />
+
-
- setCollapsed(true)}
- current={props.sequence}
- dispatch={dispatch}
- farmwareData={props.farmwareData}
- sequences={props.sequences}
- resources={props.resources}
- stepIndex={index} />
-
;
};
diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb
index 34ed04bfd..1d6502bcf 100644
--- a/spec/models/device_spec.rb
+++ b/spec/models/device_spec.rb
@@ -117,6 +117,58 @@
device.send_upgrade_request
end
+ it "builds a DB-only subquery for excess sensor readings" do
+ relation = device.excess_sensor_readings
+
+ expect(relation.to_sql).to include("OFFSET #{Device::DEFAULT_MAX_SENSOR_READINGS}")
+ expect(relation.to_sql).to include("\"sensor_readings\".\"id\" IN")
+ end
+
+ it "returns limited sensor readings in reverse chronological order" do
+ const_reassign(Device, :DEFAULT_MAX_SENSOR_READINGS, 2) do
+ oldest = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: 2.seconds.ago,
+ updated_at: 2.seconds.ago)
+ tied_time = 1.second.ago
+ older_tied = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: tied_time,
+ updated_at: tied_time)
+ newer_tied = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: tied_time,
+ updated_at: tied_time)
+
+ expect(device.limited_sensor_readings_list.pluck(:id))
+ .to eq([newer_tied.id, older_tied.id])
+ expect(device.limited_sensor_readings_list.pluck(:id))
+ .not_to include(oldest.id)
+ end
+ end
+
+ it "trims older sensor readings beyond the device limit" do
+ const_reassign(Device, :DEFAULT_MAX_SENSOR_READINGS, 2) do
+ oldest = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: 3.seconds.ago,
+ updated_at: 3.seconds.ago)
+ middle = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: 2.seconds.ago,
+ updated_at: 2.seconds.ago)
+ newest = FactoryBot.create(:sensor_reading,
+ device: device,
+ created_at: 1.second.ago,
+ updated_at: 1.second.ago)
+
+ device.trim_excess_sensor_readings
+
+ expect(device.sensor_readings.pluck(:id)).to match_array([middle.id, newest.id])
+ expect(SensorReading.exists?(oldest.id)).to be(false)
+ end
+ end
+
it "reports unknown location in feedback payload when coordinates are missing" do
expect(Faraday).to receive(:post) do |_url, payload, _headers|
text = JSON.parse(payload)["text"]