continue feature versions dev
parent
aa672e533d
commit
3b4ec0815c
|
@ -1,7 +1,9 @@
|
|||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import { error, success } from "farmbot-toastr";
|
||||
import { fetchReleases } from "../devices/actions";
|
||||
import {
|
||||
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL
|
||||
} from "../devices/actions";
|
||||
import { push } from "../history";
|
||||
import { AuthState } from "./interfaces";
|
||||
import { ReduxAction, Thunk } from "../redux/interfaces";
|
||||
|
@ -27,6 +29,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
|
|||
beta_os_update_server && beta_os_update_server != "NOT_SET" &&
|
||||
dispatch(fetchReleases(beta_os_update_server, { beta: true }));
|
||||
dispatch(getFirstPartyFarmwareList());
|
||||
dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL));
|
||||
dispatch(setToken(authState));
|
||||
Sync.fetchSyncData(dispatch);
|
||||
dispatch(connectDevice(authState));
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
badVersion
|
||||
} from "../devices/actions";
|
||||
import { init } from "../api/crud";
|
||||
import { versionOK } from "../devices/reducer";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { TaggedResource, SpecialStatus } from "../resources/tagged_resources";
|
||||
import { autoSync } from "./auto_sync";
|
||||
|
@ -24,6 +23,7 @@ import { startPinging } from "./ping_mqtt";
|
|||
import { talk } from "browser-speech";
|
||||
import { getWebAppConfigValue } from "../config_storage/actions";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import { versionOK } from "../util";
|
||||
export const TITLE = "New message from bot";
|
||||
|
||||
/** TODO: This ought to be stored in Redux. It is here because of historical
|
||||
|
|
|
@ -480,6 +480,7 @@ export enum Actions {
|
|||
BOT_CHANGE = "BOT_CHANGE",
|
||||
FETCH_OS_UPDATE_INFO_OK = "FETCH_OS_UPDATE_INFO_OK",
|
||||
FETCH_BETA_OS_UPDATE_INFO_OK = "FETCH_BETA_OS_UPDATE_INFO_OK",
|
||||
FETCH_MIN_OS_FEATURE_INFO_OK = "FETCH_MIN_OS_FEATURE_INFO_OK",
|
||||
INVERT_JOG_BUTTON = "INVERT_JOG_BUTTON",
|
||||
DISPLAY_ENCODER_DATA = "DISPLAY_ENCODER_DATA",
|
||||
STASH_STATUS = "STASH_STATUS",
|
||||
|
|
|
@ -307,6 +307,48 @@ describe("fetchReleases()", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("fetchMinOsFeatureData()", () => {
|
||||
it("fetches min OS feature data: empty", async () => {
|
||||
mockGetRelease = Promise.resolve({ data: {} });
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchMinOsFeatureData("url")(dispatch, jest.fn());
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
expect(mockError).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: "{}",
|
||||
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches min OS feature data", async () => {
|
||||
mockGetRelease = Promise.resolve({
|
||||
data: {
|
||||
"a_feature": "1.0.0", "b_feature": "2.0.0"
|
||||
}
|
||||
});
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchMinOsFeatureData("url")(dispatch, jest.fn());
|
||||
expect(axios.get).toHaveBeenCalledWith("url");
|
||||
expect(mockError).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: "{\"a_feature\":\"1.0.0\",\"b_feature\":\"2.0.0\"}",
|
||||
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK
|
||||
});
|
||||
});
|
||||
|
||||
it("fails to fetch min OS feature data", async () => {
|
||||
mockGetRelease = Promise.reject("error");
|
||||
const dispatch = jest.fn();
|
||||
await actions.fetchMinOsFeatureData("url")(dispatch, jest.fn());
|
||||
await expect(axios.get).toHaveBeenCalledWith("url");
|
||||
expect(mockError).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: "error",
|
||||
type: "FETCH_MIN_OS_FEATURE_INFO_ERROR"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateConfig()", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { versionOK, botReducer, initialState } from "../reducer";
|
||||
import { botReducer, initialState } from "../reducer";
|
||||
import { Actions } from "../../constants";
|
||||
import { ControlPanelState } from "../interfaces";
|
||||
import * as _ from "lodash";
|
||||
|
@ -6,20 +6,6 @@ import { defensiveClone } from "../../util";
|
|||
import { networkUp, networkDown } from "../../connectivity/actions";
|
||||
import { stash } from "../../connectivity/data_consistency";
|
||||
|
||||
describe("safeStringFetch", () => {
|
||||
it("Checks the correct version on update", () => {
|
||||
expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 1)).toBeTruthy();
|
||||
expect(versionOK("3.1.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("2.0.-", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("2.9.4", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("1.9.6", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("3.1.6", 4, 0)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("botRedcuer", () => {
|
||||
it("Starts / stops an update", () => {
|
||||
const step1 = botReducer(initialState(), {
|
||||
|
@ -71,6 +57,22 @@ describe("botRedcuer", () => {
|
|||
expect(r).toBe("1.2.3");
|
||||
});
|
||||
|
||||
it("fetches beta OS update info", () => {
|
||||
const r = botReducer(initialState(), {
|
||||
type: Actions.FETCH_BETA_OS_UPDATE_INFO_OK,
|
||||
payload: { version: "1.2.3", commit: undefined }
|
||||
}).currentBetaOSVersion;
|
||||
expect(r).toBe("1.2.3");
|
||||
});
|
||||
|
||||
it("fetches min OS feature data", () => {
|
||||
const r = botReducer(initialState(), {
|
||||
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK,
|
||||
payload: "{}"
|
||||
}).minOsFeatureData;
|
||||
expect(r).toBe("{}");
|
||||
});
|
||||
|
||||
it("resets hardware state when transitioning into mainenance mode.", () => {
|
||||
const state = initialState();
|
||||
const payload = defensiveClone(state.hardware);
|
||||
|
|
|
@ -14,8 +14,7 @@ import { API } from "../api/index";
|
|||
import { User } from "../auth/interfaces";
|
||||
import { getDeviceAccountSettings } from "../resources/selectors";
|
||||
import { TaggedDevice } from "../resources/tagged_resources";
|
||||
import { versionOK } from "./reducer";
|
||||
import { HttpData, oneOf } from "../util";
|
||||
import { HttpData, oneOf, versionOK } from "../util";
|
||||
import { Actions, Content } from "../constants";
|
||||
import { mcuParamValidator } from "./update_interceptor";
|
||||
import { pingAPI } from "../connectivity/ping_mqtt";
|
||||
|
@ -27,6 +26,9 @@ const ON = 1, OFF = 0;
|
|||
export type ConfigKey = keyof McuParams;
|
||||
export const EXPECTED_MAJOR = 5;
|
||||
export const EXPECTED_MINOR = 0;
|
||||
export const FEATURE_MIN_VERSIONS_URL =
|
||||
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
|
||||
"FEATURE_MIN_VERSIONS.json";
|
||||
// Already filtering messages in FarmBot OS and the API- this is just for
|
||||
// an additional layer of safety. If sensitive data ever hits a client, it will
|
||||
// be reported to Rollbar for investigation.
|
||||
|
@ -166,6 +168,24 @@ export let fetchReleases =
|
|||
});
|
||||
};
|
||||
|
||||
export let fetchMinOsFeatureData = (url: string) =>
|
||||
(dispatch: Function, getState: Function) => {
|
||||
axios
|
||||
.get(url)
|
||||
.then((resp: HttpData<string>) => {
|
||||
dispatch({
|
||||
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK,
|
||||
payload: JSON.stringify(resp.data)
|
||||
});
|
||||
})
|
||||
.catch((ferror) => {
|
||||
dispatch({
|
||||
type: "FETCH_MIN_OS_FEATURE_INFO_ERROR",
|
||||
payload: ferror
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function save(input: TaggedDevice) {
|
||||
return function (dispatch: Function, getState: GetState) {
|
||||
return axios
|
||||
|
|
|
@ -65,6 +65,8 @@ export interface BotState {
|
|||
currentBetaOSVersion?: string;
|
||||
/** The current beta os commit on the github release api */
|
||||
currentBetaOSCommit?: string;
|
||||
/** JSON string of minimum required FBOS versions for various features. */
|
||||
minOsFeatureData?: string;
|
||||
/** Is the bot in sync with the api */
|
||||
dirty: boolean;
|
||||
/** The state of the bot, as reported by the bot over MQTT. */
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
import { generateReducer } from "../redux/generate_reducer";
|
||||
import { Actions } from "../constants";
|
||||
import { EncoderDisplay } from "../controls/interfaces";
|
||||
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
import {
|
||||
maybeNegateStatus, maybeNegateConsistency
|
||||
|
@ -13,31 +12,14 @@ import { EdgeStatus } from "../connectivity/interfaces";
|
|||
import { ReduxAction } from "../redux/interfaces";
|
||||
import { connectivityReducer } from "../connectivity/reducer";
|
||||
import { BooleanConfigKey } from "../config_storage/web_app_configs";
|
||||
import { versionOK } from "../util";
|
||||
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
|
||||
|
||||
const afterEach = (state: BotState, a: ReduxAction<{}>) => {
|
||||
state.connectivity = connectivityReducer(state.connectivity, a);
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Refactor this method to use semverCompare() now that it is a thing.
|
||||
* - RC 16 Jun 2017.
|
||||
*/
|
||||
export function versionOK(stringyVersion = "0.0.0",
|
||||
_EXPECTED_MAJOR = EXPECTED_MAJOR,
|
||||
_EXPECTED_MINOR = EXPECTED_MINOR) {
|
||||
const [actual_major, actual_minor] = stringyVersion
|
||||
.split(".")
|
||||
.map(x => parseInt(x, 10));
|
||||
if (actual_major > _EXPECTED_MAJOR) {
|
||||
return true;
|
||||
} else {
|
||||
const majorOK = (actual_major == _EXPECTED_MAJOR);
|
||||
const minorOK = (actual_minor >= _EXPECTED_MINOR);
|
||||
return (majorOK && minorOK);
|
||||
}
|
||||
}
|
||||
|
||||
export let initialState = (): BotState => ({
|
||||
consistent: true,
|
||||
stepSize: 100,
|
||||
|
@ -89,6 +71,7 @@ export let initialState = (): BotState => ({
|
|||
dirty: false,
|
||||
currentOSVersion: undefined,
|
||||
currentBetaOSVersion: undefined,
|
||||
minOsFeatureData: undefined,
|
||||
connectivity: {
|
||||
"bot.mqtt": undefined,
|
||||
"user.mqtt": undefined,
|
||||
|
@ -155,6 +138,10 @@ export let botReducer = generateReducer<BotState>(initialState(), afterEach)
|
|||
s.currentBetaOSCommit = payload.commit;
|
||||
return s;
|
||||
})
|
||||
.add<string>(Actions.FETCH_MIN_OS_FEATURE_INFO_OK, (s, { payload }) => {
|
||||
s.minOsFeatureData = payload;
|
||||
return s;
|
||||
})
|
||||
.add<HardwareState>(Actions.BOT_CHANGE, (state, { payload }) => {
|
||||
state.hardware = payload;
|
||||
const { informational_settings } = state.hardware;
|
||||
|
@ -180,7 +167,8 @@ export let botReducer = generateReducer<BotState>(initialState(), afterEach)
|
|||
|
||||
const nextSyncStatus = maybeNegateStatus(info);
|
||||
|
||||
versionOK(informational_settings.controller_version);
|
||||
versionOK(informational_settings.controller_version,
|
||||
EXPECTED_MAJOR, EXPECTED_MINOR);
|
||||
state.hardware.informational_settings.sync_status = nextSyncStatus;
|
||||
return state;
|
||||
})
|
||||
|
|
|
@ -42,7 +42,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
|
|||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false
|
||||
},
|
||||
installedOsVersion: undefined,
|
||||
shouldDisplay: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ describe("<SequenceEditorMiddle/>", () => {
|
|||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false
|
||||
},
|
||||
installedOsVersion: undefined,
|
||||
shouldDisplay: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,12 @@ describe("<Sequences/>", () => {
|
|||
firstPartyFarmwareNames: [],
|
||||
showFirstPartyFarmware: false
|
||||
},
|
||||
installedOsVersion: undefined,
|
||||
shouldDisplay: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<Sequences {...fakeProps() } />);
|
||||
const wrapper = shallow(<Sequences {...fakeProps()} />);
|
||||
expect(wrapper.html()).toContain("Sequences");
|
||||
expect(wrapper.html()).toContain("Sequence Editor");
|
||||
expect(wrapper.html()).toContain(ToolTips.SEQUENCE_EDITOR);
|
||||
|
@ -46,7 +46,7 @@ describe("<Sequences/>", () => {
|
|||
it("step command cluster is hidden", () => {
|
||||
const p = fakeProps();
|
||||
p.sequence = undefined;
|
||||
const wrapper = shallow(<Sequences {...p } />);
|
||||
const wrapper = shallow(<Sequences {...p} />);
|
||||
expect(wrapper.text()).not.toContain("Commands");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ jest.mock("react-redux", () => ({
|
|||
|
||||
import { mapStateToProps } from "../state_to_props";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { TaggedDevice } from "../../resources/tagged_resources";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props", () => {
|
||||
|
@ -14,28 +13,12 @@ describe("mapStateToProps()", () => {
|
|||
expect(returnedProps.syncStatus).toEqual("unknown");
|
||||
});
|
||||
|
||||
const checkVersionResult =
|
||||
(bot: string | undefined,
|
||||
api: string | undefined,
|
||||
expected: string | undefined) => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.informational_settings.controller_version = bot;
|
||||
(state.resources.index
|
||||
.references[state.resources.index.byKind.Device[0]] as TaggedDevice)
|
||||
.body.fbos_version = api;
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.installedOsVersion).toEqual(expected);
|
||||
};
|
||||
|
||||
it("returns correct installed FBOS version string", () => {
|
||||
checkVersionResult(undefined, undefined, undefined);
|
||||
checkVersionResult("1.1.1", undefined, "1.1.1");
|
||||
checkVersionResult(undefined, "1.1.1", "1.1.1");
|
||||
checkVersionResult("bad", undefined, undefined);
|
||||
checkVersionResult(undefined, "bad", undefined);
|
||||
checkVersionResult("bad", "1.1.1", "1.1.1");
|
||||
checkVersionResult("1.2.3", "2.3.4", "2.3.4");
|
||||
checkVersionResult("1.0.1", "1.0.0", "1.0.1");
|
||||
it("returns shouldDisplay()", () => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.informational_settings.controller_version = "2.0.0";
|
||||
state.bot.minOsFeatureData = "{\"new_feature\": \"1.0.0\"}";
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.shouldDisplay("some_feature")).toBeFalsy();
|
||||
expect(props.shouldDisplay("new_feature")).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import { StepDragger } from "../draggable/step_dragger";
|
|||
import { renderCeleryNode } from "./step_tiles/index";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { getStepTag } from "../resources/sequence_tagging";
|
||||
import { HardwareFlags, FarmwareInfo } from "./interfaces";
|
||||
import { HardwareFlags, FarmwareInfo, ShouldDisplay } from "./interfaces";
|
||||
|
||||
interface AllStepsProps {
|
||||
sequence: TaggedSequence;
|
||||
|
@ -15,13 +15,13 @@ interface AllStepsProps {
|
|||
resources: ResourceIndex;
|
||||
hardwareFlags?: HardwareFlags;
|
||||
farmwareInfo?: FarmwareInfo;
|
||||
installedOsVersion?: string | undefined;
|
||||
shouldDisplay?: ShouldDisplay;
|
||||
}
|
||||
|
||||
export class AllSteps extends React.Component<AllStepsProps, {}> {
|
||||
render() {
|
||||
const {
|
||||
sequence, onDrop, dispatch, hardwareFlags, farmwareInfo, installedOsVersion
|
||||
sequence, onDrop, dispatch, hardwareFlags, farmwareInfo, shouldDisplay
|
||||
} = this.props;
|
||||
const items = (sequence.body.body || [])
|
||||
.map((currentStep: SequenceBodyItem, index, arr) => {
|
||||
|
@ -48,7 +48,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
|
|||
resources: this.props.resources,
|
||||
hardwareFlags,
|
||||
farmwareInfo,
|
||||
installedOsVersion,
|
||||
shouldDisplay,
|
||||
})}
|
||||
</div>
|
||||
</StepDragger>
|
||||
|
|
|
@ -32,7 +32,7 @@ export interface Props {
|
|||
autoSyncEnabled: boolean;
|
||||
hardwareFlags: HardwareFlags;
|
||||
farmwareInfo: FarmwareInfo;
|
||||
installedOsVersion: string | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface SequenceEditorMiddleProps {
|
||||
|
@ -44,7 +44,7 @@ export interface SequenceEditorMiddleProps {
|
|||
autoSyncEnabled: boolean;
|
||||
hardwareFlags: HardwareFlags;
|
||||
farmwareInfo: FarmwareInfo;
|
||||
installedOsVersion: string | undefined;
|
||||
shouldDisplay: ShouldDisplay;
|
||||
}
|
||||
|
||||
export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
|
||||
|
@ -54,6 +54,8 @@ export interface ActiveMiddleProps extends SequenceEditorMiddleProps {
|
|||
autoSyncEnabled: boolean;
|
||||
}
|
||||
|
||||
export type ShouldDisplay = (x: string) => boolean;
|
||||
|
||||
export type ChannelName = ALLOWED_CHANNEL_NAMES;
|
||||
|
||||
export const NUMERIC_FIELDS = ["milliseconds", "pin_mode", "pin_number",
|
||||
|
@ -169,5 +171,5 @@ export interface StepParams {
|
|||
resources: ResourceIndex;
|
||||
hardwareFlags?: HardwareFlags;
|
||||
farmwareInfo?: FarmwareInfo;
|
||||
installedOsVersion?: string | undefined;
|
||||
shouldDisplay?: ShouldDisplay;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export class SequenceEditorMiddle
|
|||
autoSyncEnabled={false}
|
||||
hardwareFlags={hardwareFlags}
|
||||
farmwareInfo={farmwareInfo}
|
||||
installedOsVersion={this.props.installedOsVersion} />;
|
||||
shouldDisplay={this.props.shouldDisplay} />;
|
||||
} else {
|
||||
return <SequenceEditorMiddleInactive />;
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export class Sequences extends React.Component<Props, {}> {
|
|||
autoSyncEnabled={this.props.autoSyncEnabled}
|
||||
hardwareFlags={this.props.hardwareFlags}
|
||||
farmwareInfo={this.props.farmwareInfo}
|
||||
installedOsVersion={this.props.installedOsVersion} />
|
||||
shouldDisplay={this.props.shouldDisplay} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col sm={3}>
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { Everything } from "../interfaces";
|
||||
import { Props, HardwareFlags } from "./interfaces";
|
||||
import {
|
||||
selectAllSequences,
|
||||
findSequence,
|
||||
maybeGetDevice
|
||||
} from "../resources/selectors";
|
||||
import { selectAllSequences, findSequence, maybeGetDevice } from "../resources/selectors";
|
||||
import { getStepTag } from "../resources/sequence_tagging";
|
||||
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
|
||||
import { betterCompact, semverCompare, SemverResult } from "../util";
|
||||
import { isUndefined } from "lodash";
|
||||
import { betterCompact, shouldDisplay, determineInstalledOsVersion } from "../util";
|
||||
import { getWebAppConfig } from "../resources/selectors";
|
||||
|
||||
export function mapStateToProps(props: Everything): Props {
|
||||
|
@ -55,22 +50,8 @@ export function mapStateToProps(props: Everything): Props {
|
|||
const conf = getWebAppConfig(props.resources.index);
|
||||
const showFirstPartyFarmware = !!(conf && conf.body.show_first_party_farmware);
|
||||
|
||||
const installedOsVersion = (): string | undefined => {
|
||||
const fromBotState = props.bot.hardware
|
||||
.informational_settings.controller_version;
|
||||
const device = maybeGetDevice(props.resources.index);
|
||||
const fromAPI = device ? device.body.fbos_version : undefined;
|
||||
if (isUndefined(fromBotState) && isUndefined(fromAPI)) { return undefined; }
|
||||
switch (semverCompare(fromBotState || "", fromAPI || "")) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return fromBotState === "" ? undefined : fromBotState;
|
||||
case SemverResult.RIGHT_IS_GREATER:
|
||||
return fromAPI === "" ? undefined : fromAPI;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const installedOsVersion = determineInstalledOsVersion(
|
||||
props.bot, maybeGetDevice(props.resources.index));
|
||||
|
||||
return {
|
||||
dispatch: props.dispatch,
|
||||
|
@ -91,6 +72,6 @@ export function mapStateToProps(props: Everything): Props {
|
|||
firstPartyFarmwareNames,
|
||||
showFirstPartyFarmware
|
||||
},
|
||||
installedOsVersion: installedOsVersion(),
|
||||
shouldDisplay: shouldDisplay(installedOsVersion, props.bot.minOsFeatureData),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "not displayed";
|
||||
p.body.label = "not displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDowns(ri.index, undefined);
|
||||
const result = pinsAsDropDowns(ri.index, x => false);
|
||||
expect(JSON.stringify(result)).not.toContain("not displayed");
|
||||
});
|
||||
|
||||
|
@ -123,7 +123,7 @@ describe("Pin and Peripheral support files", () => {
|
|||
s.body.label = "displayed";
|
||||
p.body.label = "displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = pinsAsDropDowns(ri.index, "1000.0.0");
|
||||
const result = pinsAsDropDowns(ri.index, x => true);
|
||||
expect(JSON.stringify(result)).toContain("displayed");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
selectAllSavedSensors
|
||||
} from "../../resources/selectors";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { shouldDisplay } from "../../util";
|
||||
import { DropDownItem } from "../../ui";
|
||||
import { range, isNumber, isString } from "lodash";
|
||||
import {
|
||||
|
@ -13,7 +12,7 @@ import {
|
|||
import { ReadPin, AllowedPinTypes, NamedPin } from "farmbot";
|
||||
import { bail } from "../../util/errors";
|
||||
import { joinKindAndId } from "../../resources/reducer";
|
||||
import { StepParams } from "../interfaces";
|
||||
import { StepParams, ShouldDisplay } from "../interfaces";
|
||||
import { editStep } from "../../api/crud";
|
||||
|
||||
/** `headingIds` required to group the three kinds of pins. */
|
||||
|
@ -74,11 +73,9 @@ export function pinDropdowns(
|
|||
}
|
||||
|
||||
export const pinsAsDropDowns =
|
||||
(input: ResourceIndex, fbosVersion: string | undefined): DropDownItem[] => [
|
||||
...(shouldDisplay("named_pins", fbosVersion) ?
|
||||
peripheralsAsDropDowns(input) : []),
|
||||
...(shouldDisplay("named_pins", fbosVersion) ?
|
||||
sensorsAsDropDowns(input) : []),
|
||||
(input: ResourceIndex, shouldDisplay: ShouldDisplay): DropDownItem[] => [
|
||||
...(shouldDisplay("named_pins") ? peripheralsAsDropDowns(input) : []),
|
||||
...(shouldDisplay("named_pins") ? sensorsAsDropDowns(input) : []),
|
||||
...pinDropdowns(n => n),
|
||||
];
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ export function TileIf(props: StepParams) {
|
|||
dispatch={props.dispatch}
|
||||
index={props.index}
|
||||
resources={props.resources}
|
||||
installedOsVersion={props.installedOsVersion} />;
|
||||
shouldDisplay={props.shouldDisplay} />;
|
||||
} else {
|
||||
return <p> Expected "_if" node</p>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("<If_/>", () => {
|
|||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
resources: emptyState().index,
|
||||
installedOsVersion: undefined
|
||||
shouldDisplay: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ function fakeProps(): IfParams {
|
|||
dispatch: jest.fn(),
|
||||
index: 0,
|
||||
resources: fakeResourceIndex,
|
||||
installedOsVersion: undefined,
|
||||
shouldDisplay: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ describe("LHSOptions()", () => {
|
|||
s.body.label = "not displayed";
|
||||
p.body.label = "not displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = JSON.stringify(LHSOptions(ri.index, undefined));
|
||||
const result = JSON.stringify(LHSOptions(ri.index, x => false));
|
||||
expect(result).not.toContain("not displayed");
|
||||
expect(result).toContain("X position");
|
||||
expect(result).toContain("Pin 25");
|
||||
|
@ -83,7 +83,7 @@ describe("LHSOptions()", () => {
|
|||
s.body.label = "displayed";
|
||||
p.body.label = "displayed";
|
||||
const ri = buildResourceIndex([s, p]);
|
||||
const result = JSON.stringify(LHSOptions(ri.index, "1000.0.0"));
|
||||
const result = JSON.stringify(LHSOptions(ri.index, x => true));
|
||||
expect(result).toContain("displayed");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ export function If_(props: IfParams) {
|
|||
dispatch,
|
||||
currentStep,
|
||||
index,
|
||||
installedOsVersion,
|
||||
shouldDisplay,
|
||||
} = props;
|
||||
const step = props.currentStep;
|
||||
const sequence = props.currentSequence;
|
||||
|
@ -41,7 +41,7 @@ export function If_(props: IfParams) {
|
|||
};
|
||||
}
|
||||
|
||||
const lhsOptions = LHSOptions(props.resources, installedOsVersion);
|
||||
const lhsOptions = LHSOptions(props.resources, shouldDisplay || (x => false));
|
||||
|
||||
return <Row>
|
||||
<Col xs={12}>
|
||||
|
|
|
@ -10,13 +10,14 @@ import { isRecursive } from "../index";
|
|||
import { If_ } from "./if";
|
||||
import { Then } from "./then";
|
||||
import { Else } from "./else";
|
||||
import { defensiveClone, shouldDisplay } from "../../../util";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import { overwrite } from "../../../api/crud";
|
||||
import { ToolTips } from "../../../constants";
|
||||
import { StepWrapper, StepHeader, StepContent } from "../../step_ui/index";
|
||||
import {
|
||||
sensorsAsDropDowns, peripheralsAsDropDowns, pinDropdowns
|
||||
} from "../pin_and_peripheral_support";
|
||||
import { ShouldDisplay } from "../../interfaces";
|
||||
|
||||
export interface IfParams {
|
||||
currentSequence: TaggedSequence;
|
||||
|
@ -24,7 +25,7 @@ export interface IfParams {
|
|||
dispatch: Function;
|
||||
index: number;
|
||||
resources: ResourceIndex;
|
||||
installedOsVersion?: string | undefined;
|
||||
shouldDisplay?: ShouldDisplay;
|
||||
}
|
||||
|
||||
export type Operator = "lhs"
|
||||
|
@ -34,16 +35,14 @@ export type Operator = "lhs"
|
|||
| "_else";
|
||||
|
||||
export const LHSOptions =
|
||||
(resources: ResourceIndex, fbosVersion: string | undefined
|
||||
(resources: ResourceIndex, shouldDisplay: ShouldDisplay
|
||||
): DropDownItem[] => [
|
||||
{ heading: true, label: t("Positions"), value: 0 },
|
||||
{ value: "x", label: t("X position"), headingId: "Position" },
|
||||
{ value: "y", label: t("Y position"), headingId: "Position" },
|
||||
{ value: "z", label: t("Z position"), headingId: "Position" },
|
||||
...(shouldDisplay("named_pins", fbosVersion) ?
|
||||
peripheralsAsDropDowns(resources) : []),
|
||||
...(shouldDisplay("named_pins", fbosVersion) ?
|
||||
sensorsAsDropDowns(resources) : []),
|
||||
...(shouldDisplay("named_pins") ? peripheralsAsDropDowns(resources) : []),
|
||||
...(shouldDisplay("named_pins") ? sensorsAsDropDowns(resources) : []),
|
||||
...pinDropdowns(n => `pin${n}`),
|
||||
];
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export function PinMode(props: StepParams) {
|
|||
|
||||
}
|
||||
export function TileReadPin(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence, installedOsVersion
|
||||
const { dispatch, currentStep, index, currentSequence, shouldDisplay
|
||||
} = props;
|
||||
const className = "read-pin-step";
|
||||
if (currentStep.kind !== "read_pin") { throw new Error("never"); }
|
||||
|
@ -43,7 +43,7 @@ export function TileReadPin(props: StepParams) {
|
|||
<FBSelect
|
||||
selectedItem={celery2DropDown(pin_number, props.resources)}
|
||||
onChange={setArgsDotPinNumber(props)}
|
||||
list={pinsAsDropDowns(props.resources, installedOsVersion)} />
|
||||
list={pinsAsDropDowns(props.resources, shouldDisplay || (x => false))} />
|
||||
</Col>
|
||||
<Col xs={6} md={3}>
|
||||
<label>{t("Data Label")}</label>
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from "./pin_and_peripheral_support";
|
||||
|
||||
export function TileWritePin(props: StepParams) {
|
||||
const { dispatch, currentStep, index, currentSequence, installedOsVersion
|
||||
const { dispatch, currentStep, index, currentSequence, shouldDisplay
|
||||
} = props;
|
||||
if (currentStep.kind !== "write_pin") { throw new Error("never"); }
|
||||
|
||||
|
@ -54,7 +54,7 @@ export function TileWritePin(props: StepParams) {
|
|||
<FBSelect
|
||||
selectedItem={celery2DropDown(pin_number, props.resources)}
|
||||
onChange={setArgsDotPinNumber(props)}
|
||||
list={pinsAsDropDowns(props.resources, installedOsVersion)} />
|
||||
list={pinsAsDropDowns(props.resources, shouldDisplay || (x => false))} />
|
||||
</Col>
|
||||
<Col xs={6} md={3}>
|
||||
<label>{t("Value")}</label>
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { semverCompare, SemverResult, minFwVersionCheck } from "../version";
|
||||
import { shouldDisplay } from "..";
|
||||
import {
|
||||
semverCompare,
|
||||
SemverResult,
|
||||
minFwVersionCheck,
|
||||
shouldDisplay,
|
||||
determineInstalledOsVersion,
|
||||
versionOK,
|
||||
} from "../version";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("semver compare", () => {
|
||||
it("knows when RIGHT_IS_GREATER: numeric", () => {
|
||||
|
@ -88,13 +96,61 @@ describe("minFwVersionCheck()", () => {
|
|||
});
|
||||
|
||||
describe("shouldDisplay()", () => {
|
||||
const fakeMinOsData = JSON.stringify({ some_feature: "1.0.0" });
|
||||
|
||||
it("should display", () => {
|
||||
expect(shouldDisplay("named_pin", "1000.0.0")).toBeTruthy();
|
||||
expect(shouldDisplay("1.0.0", fakeMinOsData)("some_feature")).toBeTruthy();
|
||||
expect(shouldDisplay("10.0.0", fakeMinOsData)("some_feature")).toBeTruthy();
|
||||
expect(shouldDisplay("10.0.0",
|
||||
"{\"some_feature\": \"1.0.0\"}")("some_feature")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shouldn't display", () => {
|
||||
expect(shouldDisplay("named_pin", "1.0.0")).toBeFalsy();
|
||||
expect(shouldDisplay("named_pin", undefined)).toBeFalsy();
|
||||
expect(shouldDisplay("new_feature", "1.0.0")).toBeFalsy();
|
||||
expect(shouldDisplay("0.9.0", fakeMinOsData)("some_feature")).toBeFalsy();
|
||||
expect(shouldDisplay(undefined, fakeMinOsData)("some_feature")).toBeFalsy();
|
||||
expect(shouldDisplay("1.0.0", fakeMinOsData)("other_feature")).toBeFalsy();
|
||||
expect(shouldDisplay("1.0.0", undefined)("other_feature")).toBeFalsy();
|
||||
expect(shouldDisplay("1.0.0", "")("other_feature")).toBeFalsy();
|
||||
expect(shouldDisplay("1.0.0", "{}")("other_feature")).toBeFalsy();
|
||||
expect(() => shouldDisplay("1.0.0", "bad")("other_feature"))
|
||||
.toThrowError("Error parsing 'bad', falling back to '{}'");
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineInstalledOsVersion()", () => {
|
||||
const checkVersionResult =
|
||||
(fromBot: string | undefined,
|
||||
api: string | undefined,
|
||||
expected: string | undefined) => {
|
||||
bot.hardware.informational_settings.controller_version = fromBot;
|
||||
const d = fakeDevice();
|
||||
d.body.fbos_version = api;
|
||||
const result = determineInstalledOsVersion(bot, d);
|
||||
expect(result).toEqual(expected);
|
||||
};
|
||||
|
||||
it("returns correct installed FBOS version string", () => {
|
||||
checkVersionResult(undefined, undefined, undefined);
|
||||
checkVersionResult("1.1.1", undefined, "1.1.1");
|
||||
checkVersionResult(undefined, "1.1.1", "1.1.1");
|
||||
checkVersionResult("bad", undefined, undefined);
|
||||
checkVersionResult(undefined, "bad", undefined);
|
||||
checkVersionResult("bad", "1.1.1", "1.1.1");
|
||||
checkVersionResult("1.2.3", "2.3.4", "2.3.4");
|
||||
checkVersionResult("1.0.1", "1.0.0", "1.0.1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("versionOK()", () => {
|
||||
it("checks if major/minor version meets min requirement", () => {
|
||||
expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("4.0.0", 3, 1)).toBeTruthy();
|
||||
expect(versionOK("3.1.0", 3, 0)).toBeTruthy();
|
||||
expect(versionOK("2.0.-", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("2.9.4", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("1.9.6", 3, 0)).toBeFalsy();
|
||||
expect(versionOK("3.1.6", 4, 0)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { isString, get } from "lodash";
|
||||
import { isString, get, isUndefined } from "lodash";
|
||||
import { BotState } from "../devices/interfaces";
|
||||
import { TaggedDevice } from "../resources/tagged_resources";
|
||||
|
||||
/**
|
||||
* semverCompare(): Determine which version string is greater.
|
||||
|
@ -84,23 +86,70 @@ export enum MinVersionOverride {
|
|||
NEVER = "999.999.999",
|
||||
}
|
||||
|
||||
const tempDataSource = JSON.stringify({ named_pins: MinVersionOverride.NEVER });
|
||||
|
||||
export function shouldDisplay(
|
||||
feature: string, current: string | undefined): boolean {
|
||||
if (isString(current)) {
|
||||
// TODO: get min version from JSON file
|
||||
// for example: {"some_feature": "1.2.3", "other_feature": "2.3.4"}
|
||||
const minOsVersionFeatureLookup = JSON.parse(tempDataSource);
|
||||
const min = get(minOsVersionFeatureLookup, feature,
|
||||
MinVersionOverride.NEVER);
|
||||
switch (semverCompare(current, min)) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
current: string | undefined, lookupData: string | undefined) {
|
||||
return function (feature: string): boolean {
|
||||
if (isString(current)) {
|
||||
let minOsVersionFeatureLookup = {};
|
||||
try {
|
||||
minOsVersionFeatureLookup = JSON.parse(lookupData || "{}");
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing '${lookupData}', falling back to '{}'`);
|
||||
}
|
||||
const min = get(minOsVersionFeatureLookup, feature,
|
||||
MinVersionOverride.NEVER);
|
||||
switch (semverCompare(current, min)) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* determineInstalledOsVersion(): Compare the current FBOS version in the bot's
|
||||
* state with the API's fbos_version string and return the greatest version.
|
||||
*/
|
||||
|
||||
export function determineInstalledOsVersion(
|
||||
bot: BotState, device: TaggedDevice | undefined): string | undefined {
|
||||
const fromBotState = bot.hardware.informational_settings.controller_version;
|
||||
const fromAPI = device ? device.body.fbos_version : undefined;
|
||||
if (isUndefined(fromBotState) && isUndefined(fromAPI)) { return undefined; }
|
||||
switch (semverCompare(fromBotState || "", fromAPI || "")) {
|
||||
case SemverResult.LEFT_IS_GREATER:
|
||||
case SemverResult.EQUAL:
|
||||
return fromBotState === "" ? undefined : fromBotState;
|
||||
case SemverResult.RIGHT_IS_GREATER:
|
||||
return fromAPI === "" ? undefined : fromAPI;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare installed FBOS version against the lowest version compatible
|
||||
* with the web app to lock out incompatible FBOS versions from the App.
|
||||
* It uses a different method than semverCompare() to only look at
|
||||
* major and minor numeric versions and ignores patch and pre-release
|
||||
* identifiers.
|
||||
*/
|
||||
|
||||
export function versionOK(stringyVersion = "0.0.0",
|
||||
_EXPECTED_MAJOR: number,
|
||||
_EXPECTED_MINOR: number) {
|
||||
const [actual_major, actual_minor] = stringyVersion
|
||||
.split(".")
|
||||
.map(x => parseInt(x, 10));
|
||||
if (actual_major > _EXPECTED_MAJOR) {
|
||||
return true;
|
||||
} else {
|
||||
const majorOK = (actual_major == _EXPECTED_MAJOR);
|
||||
const minorOK = (actual_minor >= _EXPECTED_MINOR);
|
||||
return (majorOK && minorOK);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue