continue feature versions dev

pull/705/head
gabrielburnworth 2018-03-07 19:42:34 -08:00
parent aa672e533d
commit 3b4ec0815c
28 changed files with 278 additions and 153 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. */

View File

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

View File

@ -42,7 +42,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false
},
installedOsVersion: undefined,
shouldDisplay: jest.fn(),
};
}

View File

@ -23,7 +23,7 @@ describe("<SequenceEditorMiddle/>", () => {
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false
},
installedOsVersion: undefined,
shouldDisplay: jest.fn(),
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ export class SequenceEditorMiddle
autoSyncEnabled={false}
hardwareFlags={hardwareFlags}
farmwareInfo={farmwareInfo}
installedOsVersion={this.props.installedOsVersion} />;
shouldDisplay={this.props.shouldDisplay} />;
} else {
return <SequenceEditorMiddleInactive />;
}

View File

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

View File

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

View File

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

View File

@ -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),
];

View File

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

View File

@ -24,7 +24,7 @@ describe("<If_/>", () => {
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
installedOsVersion: undefined
shouldDisplay: jest.fn(),
};
}

View File

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

View File

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

View File

@ -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}`),
];

View File

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

View File

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

View File

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

View File

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