Restore auto_sync to previous state
parent
5b8418b1f3
commit
fdea31246a
|
@ -30,7 +30,7 @@ You will need the following:
|
|||
0. [Docker 17.06.0-ce or greater](https://docs.docker.com/engine/installation/)
|
||||
0. [Ruby 2.4.2](http://rvm.io/rvm/install)
|
||||
0. [ImageMagick](https://www.imagemagick.org/script/index.php) (`brew install imagemagick` (Mac) or `sudo apt-get install imagemagick` (Ubuntu))
|
||||
0. [Node JS > v6](https://nodejs.org/en/download/)
|
||||
0. [Node JS >= v8.9.0](https://nodejs.org/en/download/)
|
||||
0. [`libpq-dev` and `postgresql`](http://stackoverflow.com/questions/6040583/cant-find-the-libpq-fe-h-header-when-trying-to-install-pg-gem/6040822#6040822) and `postgresql-contrib`
|
||||
0. `gem install bundler`
|
||||
|
||||
|
|
|
@ -133,5 +133,8 @@
|
|||
"json",
|
||||
"lcov"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
|
||||
export let bot: Everything["bot"] = {
|
||||
"consistent": true,
|
||||
"stepSize": 100,
|
||||
"controlPanelState": {
|
||||
"homing_and_calibration": false,
|
||||
|
|
|
@ -20,7 +20,8 @@ describe("<App />: Controls Pop-Up", () => {
|
|||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true
|
||||
consistent: true,
|
||||
autoSyncEnabled: true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -53,14 +54,16 @@ describe("<App />: Controls Pop-Up", () => {
|
|||
|
||||
describe.skip("<App />: Loading", () => {
|
||||
function fakeProps(): AppProps {
|
||||
return {
|
||||
const p: AppProps = {
|
||||
dispatch: jest.fn(),
|
||||
loaded: [],
|
||||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true
|
||||
consistent: true,
|
||||
autoSyncEnabled: true
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
it("MUST_LOADs not loaded", () => {
|
||||
|
@ -91,7 +94,8 @@ describe("<App />: NavBar", () => {
|
|||
logs: [],
|
||||
user: fakeUser(),
|
||||
bot: bot,
|
||||
consistent: true
|
||||
consistent: true,
|
||||
autoSyncEnabled: true
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ResourceIndex } from "../resources/interfaces";
|
|||
import { SequenceBodyItem } from "farmbot/dist";
|
||||
import * as _ from "lodash";
|
||||
import { Actions } from "../constants";
|
||||
import { startTracking } from "../connectivity/data_consistency";
|
||||
|
||||
export function edit(tr: TaggedResource, changes: Partial<typeof tr.body>):
|
||||
ReduxAction<EditResourceParams> {
|
||||
|
@ -131,6 +132,7 @@ export function refreshNO(payload: GeneralizedError): ReduxAction<GeneralizedErr
|
|||
|
||||
function update(uuid: string) {
|
||||
return function (dispatch: Function, getState: GetState) {
|
||||
maybeStartTracking(uuid);
|
||||
return updateViaAjax(getState().resources.index, uuid, dispatch);
|
||||
};
|
||||
}
|
||||
|
@ -141,6 +143,7 @@ export function destroy(uuid: string, force = false) {
|
|||
const maybeProceed = confirmationChecker(resource, force);
|
||||
return maybeProceed(() => {
|
||||
if (resource.body.id) {
|
||||
maybeStartTracking(uuid);
|
||||
return axios
|
||||
.delete(urlFor(resource.kind) + resource.body.id)
|
||||
.then(function (resp: HttpData<typeof resource.body>) {
|
||||
|
@ -164,7 +167,11 @@ export function saveAll(input: TaggedResource[],
|
|||
return function (dispatch: Function, getState: GetState) {
|
||||
const p = input
|
||||
.filter(x => x.specialStatus === SpecialStatus.DIRTY)
|
||||
.map(tts => dispatch(save(tts.uuid)));
|
||||
.map(tts => tts.uuid)
|
||||
.map(uuid => {
|
||||
maybeStartTracking(uuid);
|
||||
return dispatch(save(uuid));
|
||||
});
|
||||
Promise.all(p).then(callback, errBack);
|
||||
};
|
||||
}
|
||||
|
@ -206,6 +213,7 @@ function updateViaAjax(index: ResourceIndex,
|
|||
} else {
|
||||
verb = "post";
|
||||
}
|
||||
maybeStartTracking(uuid);
|
||||
return axios[verb](url, body)
|
||||
.then(function (resp: HttpData<typeof resource.body>) {
|
||||
const r1 = defensiveClone(resource);
|
||||
|
@ -242,3 +250,14 @@ const confirmationChecker = (resource: TaggedResource, force = false) =>
|
|||
}
|
||||
return proceed();
|
||||
};
|
||||
|
||||
const DONT_CARE: ResourceName[] = [
|
||||
"Log",
|
||||
"Image",
|
||||
"WebcamFeed",
|
||||
];
|
||||
|
||||
function maybeStartTracking(uuid: string) {
|
||||
const forgetAboutIt = DONT_CARE.includes(uuid.split(".")[0] as ResourceName);
|
||||
return forgetAboutIt || startTracking();
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface AppProps {
|
|||
user: TaggedUser | undefined;
|
||||
bot: BotState;
|
||||
consistent: boolean;
|
||||
autoSyncEnabled: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(props: Everything): AppProps {
|
||||
|
@ -41,7 +42,8 @@ function mapStateToProps(props: Everything): AppProps {
|
|||
.reverse()
|
||||
.value(),
|
||||
loaded: props.resources.loaded,
|
||||
consistent: props.connectivity.consistent
|
||||
consistent: !!(props.bot || {}).consistent,
|
||||
autoSyncEnabled: !!props.bot.hardware.configuration.auto_sync
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -88,7 +90,9 @@ export class App extends React.Component<AppProps, {}> {
|
|||
user={this.props.user}
|
||||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
logs={this.props.logs} />
|
||||
logs={this.props.logs}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
/>
|
||||
{!syncLoaded && <LoadingPlant />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!currentPath.startsWith("/app/controls") &&
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import { getDevice } from "../device";
|
||||
import { store } from "../redux/store";
|
||||
import { Actions } from "../constants";
|
||||
// import { store } from "../redux/store";
|
||||
// import { Actions } from "../constants";
|
||||
import { semverCompare, SemverResult } from "../util";
|
||||
import { SyncStatus } from "farmbot";
|
||||
|
||||
const outstandingRequests: Set<string> = new Set();
|
||||
(window as any)["outstanding_requests"] = outstandingRequests;
|
||||
|
||||
/** Use this when you need to throw the FE into an inconsistent state, but dont
|
||||
* have a real UUID available. It will be removed when a "real" UUID comes
|
||||
* along. This is necesary for creating an instantaneous "syncing..." label. */
|
||||
const PLACEHOLDER = "PLACEHOLDER";
|
||||
/** Max wait in MS before clearing out.
|
||||
* I based this figure off of our highest "problem response time" in production
|
||||
* plus an additional 25%. */
|
||||
const MAX_WAIT = 2000;
|
||||
/**
|
||||
* PROBLEM: You save a sequence and click "RUN" very fast. The remote device
|
||||
* did not have time to download the new sequence and so it crashes
|
||||
|
@ -34,19 +43,85 @@ const outstandingRequests: Set<string> = new Set();
|
|||
* this and we could track data operations the same way a `exec_sequence`
|
||||
* and friends.
|
||||
*/
|
||||
export function startTracking(uuid: string | undefined) {
|
||||
if (uuid) {
|
||||
store.dispatch({ type: Actions.SET_CONSISTENCY, payload: false });
|
||||
outstandingRequests.add(uuid);
|
||||
const stop = () => stopTracking(uuid);
|
||||
getDevice().on(uuid, stop);
|
||||
setTimeout(stop, 5000);
|
||||
}
|
||||
export function startTracking(uuid = PLACEHOLDER) {
|
||||
// store.dispatch({ type: Actions.SET_CONSISTENCY, payload: false });
|
||||
outstandingRequests.add(uuid);
|
||||
const stop = () => stopTracking(uuid);
|
||||
getDevice().on(uuid, stop);
|
||||
setTimeout(stop, MAX_WAIT);
|
||||
}
|
||||
|
||||
export function stopTracking(uuid: string) {
|
||||
outstandingRequests.delete(uuid);
|
||||
outstandingRequests.delete(PLACEHOLDER);
|
||||
if (outstandingRequests.size === 0) {
|
||||
store.dispatch({ type: Actions.SET_CONSISTENCY, payload: true });
|
||||
// store.dispatch({ type: Actions.SET_CONSISTENCY, payload: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** There are a bunch of ways we need to handle data consistency management
|
||||
* depending on a number of factors. */
|
||||
export enum SyncStrat {
|
||||
/** Auto sync is enabled. */
|
||||
AUTO,
|
||||
/** Auto sync is not enabled */
|
||||
MANUAL,
|
||||
/** Device does not support auto_sync in any way. */
|
||||
LEGACY,
|
||||
/** Not enough info to say. */
|
||||
OFFLINE
|
||||
}
|
||||
|
||||
/** Highest version lacking auto sync. Remove in January 2018 -RC */
|
||||
const TOO_OLD = "5.0.6";
|
||||
|
||||
interface StratHints {
|
||||
fbosVersion?: string;
|
||||
autoSync: boolean;
|
||||
}
|
||||
|
||||
export function determineStrategy(x: StratHints): SyncStrat {
|
||||
const { fbosVersion, autoSync } = x;
|
||||
/** First pass: Is it even on right now? */
|
||||
if (!fbosVersion) {
|
||||
console.log("Chose 'offline' strategy.");
|
||||
return SyncStrat.OFFLINE;
|
||||
}
|
||||
|
||||
/** Second pass: Is it an old version? */
|
||||
if (semverCompare(TOO_OLD, fbosVersion) !== SemverResult.RIGHT_IS_GREATER) {
|
||||
console.log("Chose 'legacy' strategy.");
|
||||
return SyncStrat.LEGACY;
|
||||
}
|
||||
|
||||
/** Third pass: Is auto_sync enabled? */
|
||||
const strat = autoSync ? "AUTO" : "MANUAL";
|
||||
console.log(`Chose '${strat}' strategy.`);
|
||||
return SyncStrat[strat];
|
||||
}
|
||||
|
||||
export interface OverrideHints {
|
||||
consistent: boolean;
|
||||
syncStatus: SyncStatus | undefined;
|
||||
fbosVersion: string | undefined;
|
||||
autoSync: boolean;
|
||||
}
|
||||
/** Sometimes we can't trust what FBOS tells us. */
|
||||
export function maybeNegateStatus(x: OverrideHints): SyncStatus | undefined {
|
||||
const { consistent, syncStatus, fbosVersion, autoSync } = x;
|
||||
|
||||
/** No need to override if data is consistent. */
|
||||
if (consistent) {
|
||||
return syncStatus;
|
||||
}
|
||||
|
||||
switch (determineStrategy({ autoSync, fbosVersion })) {
|
||||
case SyncStrat.AUTO:
|
||||
return "syncing";
|
||||
case SyncStrat.LEGACY:
|
||||
case SyncStrat.MANUAL:
|
||||
return "sync_now";
|
||||
case SyncStrat.OFFLINE:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,11 @@ export interface ResourceReady {
|
|||
data: [DeviceAccountSettings];
|
||||
}
|
||||
|
||||
type StatusRecord = Record<Edge, ConnectionStatus | undefined>;
|
||||
type ConnectionRecord = Record<Edge, ConnectionStatus | undefined>;
|
||||
|
||||
/** Mapping of known connection status.
|
||||
* An `undefined` value means we don't know. */
|
||||
export interface ConnectionState extends StatusRecord {
|
||||
/** Have all API requests been acknowledged by external services?
|
||||
* This flag lets us know if it is safe to do data critical tasks with the bot
|
||||
*/
|
||||
consistent: boolean;
|
||||
}
|
||||
export interface ConnectionState extends ConnectionRecord { }
|
||||
|
||||
export interface UpdateMqttData {
|
||||
status: "UPDATE"
|
||||
|
|
|
@ -6,16 +6,11 @@ import { computeBestTime } from "./reducer_support";
|
|||
export const DEFAULT_STATE: ConnectionState = {
|
||||
"bot.mqtt": undefined,
|
||||
"user.mqtt": undefined,
|
||||
"user.api": undefined,
|
||||
consistent: true
|
||||
"user.api": undefined
|
||||
};
|
||||
|
||||
export let connectivityReducer =
|
||||
generateReducer<ConnectionState>(DEFAULT_STATE)
|
||||
.add<boolean>(Actions.SET_CONSISTENCY, (s, { payload }) => {
|
||||
s.consistent = payload;
|
||||
return s;
|
||||
})
|
||||
.add<EdgeStatus>(Actions.NETWORK_EDGE_CHANGE, (s, { payload }) => {
|
||||
s[payload.name] = payload.status;
|
||||
return s;
|
||||
|
|
|
@ -387,7 +387,7 @@ export enum Actions {
|
|||
OVERWRITE_RESOURCE = "OVERWRITE_RESOURCE",
|
||||
SAVE_RESOURCE_START = "SAVE_RESOURCE_START",
|
||||
RESOURCE_READY = "RESOURCE_READY",
|
||||
_RESOURCE_NO = "*_RESOURCE_NO",
|
||||
_RESOURCE_NO = "_RESOURCE_NO",
|
||||
REFRESH_RESOURCE_START = "REFRESH_RESOURCE_START",
|
||||
REFRESH_RESOURCE_OK = "REFRESH_RESOURCE_OK",
|
||||
REFRESH_RESOURCE_NO = "REFRESH_RESOURCE_NO",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ToggleButton } from "../../../controls/toggle_button";
|
|||
import { Content } from "../../../constants";
|
||||
import { updateConfig } from "../../actions";
|
||||
import { noop } from "lodash";
|
||||
import { getDevice } from "../../../device";
|
||||
|
||||
interface AutoSyncRowProps { currentValue: boolean; }
|
||||
|
||||
|
@ -23,7 +24,10 @@ export function AutoSyncRow(props: AutoSyncRowProps) {
|
|||
</Col>
|
||||
<Col xs={3}>
|
||||
<ToggleButton toggleValue={props.currentValue}
|
||||
toggleAction={() => updateConfig({ auto_sync })(noop)} />
|
||||
toggleAction={() => {
|
||||
updateConfig({ auto_sync })(noop);
|
||||
getDevice().sync();
|
||||
}} />
|
||||
</Col>
|
||||
</Row>;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@ export interface BotState {
|
|||
axis_inversion: Record<Xyz, boolean>;
|
||||
/** The display setting for encoder data on the controls page. */
|
||||
encoder_visibility: Record<EncoderDisplay, boolean>;
|
||||
/** Have all API requests been acknowledged by external services? This flag
|
||||
* lets us know if it is safe to do data critical tasks with the bot */
|
||||
consistent: boolean;
|
||||
}
|
||||
|
||||
export interface BotProp {
|
||||
|
|
|
@ -25,6 +25,7 @@ export function versionOK(stringyVersion = "0.0.0",
|
|||
}
|
||||
}
|
||||
export let initialState: BotState = {
|
||||
consistent: true,
|
||||
stepSize: 100,
|
||||
controlPanelState: {
|
||||
homing_and_calibration: false,
|
||||
|
|
|
@ -15,7 +15,8 @@ describe("NavBar", () => {
|
|||
logs={[log]}
|
||||
bot={bot}
|
||||
user={taggedUser}
|
||||
dispatch={jest.fn()} />
|
||||
dispatch={jest.fn()}
|
||||
autoSyncEnabled={false} />
|
||||
);
|
||||
|
||||
expect(wrapper.hasClass("nav-wrapper")).toBeTruthy();
|
||||
|
@ -27,7 +28,8 @@ describe("NavBar", () => {
|
|||
logs={[log]}
|
||||
bot={bot}
|
||||
user={taggedUser}
|
||||
dispatch={jest.fn()} />);
|
||||
dispatch={jest.fn()}
|
||||
autoSyncEnabled={false} />);
|
||||
const link = wrapper.find("Link").first();
|
||||
link.simulate("click");
|
||||
expect(wrapper.state().mobileMenuOpen).toBeFalsy();
|
||||
|
|
|
@ -11,7 +11,9 @@ describe("<SyncButton/>", function () {
|
|||
const dispatcher = jest.fn();
|
||||
const result = shallow(<SyncButton user={undefined}
|
||||
dispatch={dispatcher}
|
||||
bot={FAKE_BOT_STATE} />);
|
||||
bot={FAKE_BOT_STATE}
|
||||
consistent={true}
|
||||
autoSyncEnabled={true} />);
|
||||
expect(result.hasClass("nav-sync")).toBeFalsy();
|
||||
expect(result.html()).toEqual("<span></span>");
|
||||
});
|
||||
|
|
|
@ -37,7 +37,9 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
|
|||
return <SyncButton
|
||||
bot={this.props.bot}
|
||||
user={this.props.user}
|
||||
dispatch={this.props.dispatch} />;
|
||||
dispatch={this.props.dispatch}
|
||||
consistent={this.props.consistent}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled} />;
|
||||
}
|
||||
render() {
|
||||
const hasName = this.props.user && this.props.user.body.name;
|
||||
|
|
|
@ -6,6 +6,8 @@ export interface NavButtonProps {
|
|||
user: TaggedUser | undefined;
|
||||
dispatch: Function;
|
||||
bot: BotState;
|
||||
consistent: boolean;
|
||||
autoSyncEnabled: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
|
@ -15,6 +17,7 @@ export interface NavBarProps {
|
|||
bot: BotState;
|
||||
user: TaggedUser | undefined;
|
||||
dispatch: Function;
|
||||
autoSyncEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface NavBarState {
|
||||
|
|
|
@ -23,7 +23,7 @@ export interface GeneralizedError {
|
|||
* requirements */
|
||||
export function generalizedError(payload: GeneralizedError) {
|
||||
toastErrors(payload);
|
||||
return { type: "*_RESOURCE_NO", payload };
|
||||
return { type: Actions._RESOURCE_NO, payload };
|
||||
}
|
||||
|
||||
export let destroyNO = generalizedError;
|
||||
|
|
|
@ -25,9 +25,7 @@ describe("<TestButton/>", () => {
|
|||
onClick: jest.fn(),
|
||||
onFail: jest.fn(),
|
||||
sequence: fakeSequence(),
|
||||
syncStatus: "synced",
|
||||
consistent: true,
|
||||
autoSyncEnabled: false
|
||||
syncStatus: "synced"
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -57,9 +57,7 @@ export class SequenceEditorMiddleActive extends
|
|||
syncStatus={this.props.syncStatus}
|
||||
sequence={sequence}
|
||||
onFail={warning}
|
||||
onClick={() => execSequence(sequence.body)}
|
||||
consistent={this.props.consistent}
|
||||
autoSyncEnabled={this.props.autoSyncEnabled} />
|
||||
onClick={() => execSequence(sequence.body)} />
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={() => dispatch(destroy(sequence.uuid))}>
|
||||
|
|
|
@ -30,7 +30,7 @@ export function mapStateToProps(props: Everything): Props {
|
|||
auth: props.auth,
|
||||
resources: props.resources.index,
|
||||
syncStatus,
|
||||
consistent: props.connectivity.consistent,
|
||||
consistent: props.bot.consistent,
|
||||
autoSyncEnabled: !!props.bot.hardware.configuration.auto_sync
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,25 +9,13 @@ export interface TestBtnProps {
|
|||
/** Callback fired is NOT synced. */
|
||||
onFail(message: string): void;
|
||||
syncStatus: SyncStatus;
|
||||
/** Are there uncommited data operations that need broadcasted to other
|
||||
* entities on the network? */
|
||||
consistent: boolean;
|
||||
autoSyncEnabled: boolean;
|
||||
sequence: TaggedSequence;
|
||||
}
|
||||
|
||||
export function TestButton(p: TestBtnProps) {
|
||||
const {
|
||||
onClick,
|
||||
onFail,
|
||||
syncStatus,
|
||||
sequence,
|
||||
autoSyncEnabled,
|
||||
consistent
|
||||
} = p;
|
||||
export function TestButton({ onClick, onFail, syncStatus, sequence }: TestBtnProps) {
|
||||
const isSynced = syncStatus === "synced";
|
||||
const isSaved = !sequence.specialStatus;
|
||||
const canTest = (isSynced && isSaved) && (!autoSyncEnabled || !consistent);
|
||||
const canTest = isSynced && isSaved;
|
||||
const className = canTest ? "orange" : "gray";
|
||||
|
||||
const clickHandler = () => (canTest) ?
|
||||
|
|
Loading…
Reference in New Issue