Restore auto_sync to previous state

pull/533/head
Rick Carlino 2017-11-20 13:44:46 -06:00
parent 5b8418b1f3
commit fdea31246a
22 changed files with 159 additions and 61 deletions

View File

@ -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`

View File

@ -133,5 +133,8 @@
"json",
"lcov"
]
},
"engines": {
"node": ">=8.9.0"
}
}

View File

@ -1,6 +1,7 @@
import { Everything } from "../../interfaces";
export let bot: Everything["bot"] = {
"consistent": true,
"stepSize": 100,
"controlPanelState": {
"homing_and_calibration": false,

View File

@ -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
};
}

View File

@ -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();
}

View File

@ -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") &&

View File

@ -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";
}
}

View File

@ -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"

View File

@ -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;

View File

@ -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",

View File

@ -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>;
}

View File

@ -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 {

View File

@ -25,6 +25,7 @@ export function versionOK(stringyVersion = "0.0.0",
}
}
export let initialState: BotState = {
consistent: true,
stepSize: 100,
controlPanelState: {
homing_and_calibration: false,

View File

@ -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();

View File

@ -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>");
});

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -25,9 +25,7 @@ describe("<TestButton/>", () => {
onClick: jest.fn(),
onFail: jest.fn(),
sequence: fakeSequence(),
syncStatus: "synced",
consistent: true,
autoSyncEnabled: false
syncStatus: "synced"
};
}

View File

@ -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))}>

View File

@ -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
};
}

View File

@ -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) ?