add destroyAll feature

pull/1031/head
gabrielburnworth 2018-11-02 12:53:17 -07:00
parent 314baf3854
commit f9f21f6e5c
12 changed files with 130 additions and 28 deletions

View File

@ -1,8 +1,8 @@
# Api::FarmwareEnvController is the RESTful endpoint for managing key/value
# configuration pairs.
module Api
# Device configs controller handles CRUD for user definable key/value pairs.
# Usually used by 3rd party Farmware devs. Not used often as of May 2018
# Farmware envs controller handles CRUD for user definable key/value pairs.
# Usually used for Farmware settings and data.
class FarmwareEnvsController < Api::AbstractController
def create
@ -22,7 +22,11 @@ module Api
end
def destroy
render json: (farmware_env.destroy! && "")
if params[:id] == "all"
render json: (current_device.farmware_envs.destroy_all && "")
else
render json: (farmware_env.destroy! && "")
end
end
private

View File

@ -13,7 +13,7 @@ module FarmwareEnvs
if device.farmware_envs.length >= LIMIT
add_error :configs,
:configs,
"You are over the limit of #{LIMIT} configs."
"You are over the limit of #{LIMIT} Farmware Envs."
end
end

View File

@ -6,7 +6,7 @@ describe Api::FarmwareEnvsController do
include Devise::Test::ControllerHelpers
it 'creates a device config' do
it 'creates a farmware env' do
sign_in user
b4 = FarmwareEnv.count
input = { key: "Coffee Emoji", value: "" }
@ -57,6 +57,15 @@ describe Api::FarmwareEnvsController do
expect(FarmwareEnv.exists?(id)).to be false
end
it 'deletes all' do
sign_in user
FarmwareEnv.destroy_all
FactoryBot.create_list(:farmware_env, 3, device: device)
delete :destroy, params: { id: "all" }
expect(response.status).to be(200)
expect(FarmwareEnv.count).to eq(0)
end
it 'shows' do
sign_in user
fe = FactoryBot.create(:farmware_env, device: device)

View File

@ -13,14 +13,14 @@ jest.mock("../maybe_start_tracking", () => ({
maybeStartTracking: jest.fn()
}));
let mockDelete: Promise<{}> = Promise.resolve({});
let mockDelete: Promise<{} | void> = Promise.resolve({});
jest.mock("axios", () => ({
default: {
delete: jest.fn(() => mockDelete)
}
}));
import { destroy } from "../crud";
import { destroy, destroyAll } from "../crud";
import { API } from "../api";
import axios from "axios";
import { destroyOK, destroyNO } from "../../resources/actions";
@ -99,3 +99,37 @@ describe("destroy", () => {
});
});
});
describe("destroyAll", () => {
it("confirmed", async () => {
window.confirm = () => true;
mockDelete = Promise.resolve();
await expect(destroyAll("FarmwareEnv")).resolves.toEqual(undefined);
expect(axios.delete)
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
});
it("confirmation overridden", async () => {
window.confirm = () => false;
mockDelete = Promise.resolve();
await expect(destroyAll("FarmwareEnv", true)).resolves.toEqual(undefined);
expect(axios.delete)
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
});
it("cancelled", async () => {
window.confirm = () => false;
mockDelete = Promise.resolve();
await expect(destroyAll("FarmwareEnv"))
.rejects.toEqual("User pressed cancel");
expect(axios.delete).not.toHaveBeenCalled();
});
it("rejected", async () => {
window.confirm = () => true;
mockDelete = Promise.reject("error");
await expect(destroyAll("FarmwareEnv")).rejects.toEqual("error");
expect(axios.delete)
.toHaveBeenCalledWith("http://localhost:3000/api/farmware_envs/all");
});
});

View File

@ -171,7 +171,7 @@ export const destroyCatch = (p: DestroyNoProps) => (err: UnsafeError) => {
export function destroy(uuid: string, force = false) {
return function (dispatch: Function, getState: GetState) {
const resource = findByUuid(getState().resources.index, uuid);
const maybeProceed = confirmationChecker(resource, force);
const maybeProceed = confirmationChecker(resource.kind, force);
return maybeProceed(() => {
const statusBeforeError = resource.specialStatus;
if (resource.body.id) {
@ -190,6 +190,14 @@ export function destroy(uuid: string, force = false) {
};
}
export function destroyAll(resourceName: ResourceName, force = false) {
if (force || confirm(t("Are you sure you want to delete all items?"))) {
return axios.delete(urlFor(resourceName) + "all");
} else {
return Promise.reject("User pressed cancel");
}
}
export function saveAll(input: TaggedResource[],
callback: () => void = _.noop,
errBack: (err: UnsafeError) => void = _.noop) {
@ -282,9 +290,9 @@ const MUST_CONFIRM_LIST: ResourceName[] = [
"SavedGarden",
];
const confirmationChecker = (resource: TaggedResource, force = false) =>
const confirmationChecker = (resourceName: ResourceName, force = false) =>
<T>(proceed: () => T): T | undefined => {
if (MUST_CONFIRM_LIST.includes(resource.kind)) {
if (MUST_CONFIRM_LIST.includes(resourceName)) {
if (force || confirm(t("Are you sure you want to delete this item?"))) {
return proceed();
} else {

View File

@ -757,6 +757,12 @@ ul {
float: right !important;
}
.farmware-settings-menu-contents {
label {
margin-top: 0.5rem;
}
}
.logs {
.row {
@media screen and (max-width: 974px) {

View File

@ -5,30 +5,33 @@ const mockDevice = {
execScript: jest.fn(() => Promise.resolve()),
installFirstPartyFarmware: jest.fn(() => Promise.resolve())
};
jest.mock("../../device", () => ({
getDevice: () => (mockDevice)
}));
jest.mock("../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../../config_storage/actions", () => ({
toggleWebAppBool: jest.fn()
}));
let mockDestroyAllPromise: Promise<void | never> = Promise.reject("error");
jest.mock("../../api/crud", () => ({
destroyAll: jest.fn(() => mockDestroyAllPromise)
}));
import * as React from "react";
import { mount } from "enzyme";
import { FarmwareConfigMenu } from "../farmware_config_menu";
import { FarmwareConfigMenuProps } from "../interfaces";
import { getDevice } from "../../device";
import { toggleWebAppBool } from "../../config_storage/actions";
import { destroyAll } from "../../api/crud";
import { success, error } from "farmbot-toastr";
describe("<FarmwareConfigMenu />", () => {
function fakeProps(): FarmwareConfigMenuProps {
return {
show: true,
dispatch: jest.fn(),
firstPartyFwsInstalled: false
};
}
const fakeProps = (): FarmwareConfigMenuProps => ({
show: true,
dispatch: jest.fn(),
firstPartyFwsInstalled: false,
shouldDisplay: () => false,
});
it("calls install 1st party farmwares", () => {
const wrapper = mount(<FarmwareConfigMenu {...fakeProps()} />);
@ -40,8 +43,7 @@ describe("<FarmwareConfigMenu />", () => {
it("1st party farmwares all installed", () => {
const p = fakeProps();
p.firstPartyFwsInstalled = true;
const wrapper = mount(
<FarmwareConfigMenu {...p} />);
const wrapper = mount(<FarmwareConfigMenu {...p} />);
const button = wrapper.find("button").first();
expect(button.hasClass("fa-download")).toBeTruthy();
button.simulate("click");
@ -49,8 +51,7 @@ describe("<FarmwareConfigMenu />", () => {
});
it("toggles 1st party farmware display", () => {
const wrapper = mount(
<FarmwareConfigMenu {...fakeProps()} />);
const wrapper = mount(<FarmwareConfigMenu {...fakeProps()} />);
const button = wrapper.find("button").last();
expect(button.hasClass("green")).toBeTruthy();
expect(button.hasClass("fb-toggle-button")).toBeTruthy();
@ -61,9 +62,28 @@ describe("<FarmwareConfigMenu />", () => {
it("1st party farmware display is disabled", () => {
const p = fakeProps();
p.show = false;
const wrapper = mount(
<FarmwareConfigMenu {...p} />);
const wrapper = mount(<FarmwareConfigMenu {...p} />);
const button = wrapper.find("button").last();
expect(button.hasClass("red")).toBeTruthy();
});
it("destroys all FarmwareEnvs", async () => {
mockDestroyAllPromise = Promise.resolve();
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = mount(<FarmwareConfigMenu {...p} />);
wrapper.find("button").last().simulate("click");
await expect(destroyAll).toHaveBeenCalledWith("FarmwareEnv");
expect(success).toHaveBeenCalledWith(expect.stringContaining("deleted"));
});
it("fails to destroy all FarmwareEnvs", async () => {
mockDestroyAllPromise = Promise.reject("error");
const p = fakeProps();
p.shouldDisplay = () => true;
const wrapper = mount(<FarmwareConfigMenu {...p} />);
await wrapper.find("button").last().simulate("click");
await expect(destroyAll).toHaveBeenCalledWith("FarmwareEnv");
expect(error).toHaveBeenCalled();
});
});

View File

@ -21,6 +21,7 @@ describe("<FarmwareList />", () => {
farmwares: fakeFarmwares(),
showFirstParty: false,
firstPartyFarmwareNames: [],
shouldDisplay: () => false,
};
};

View File

@ -4,11 +4,14 @@ import { getDevice } from "../device";
import { FarmwareConfigMenuProps } from "./interfaces";
import { commandErr } from "../devices/actions";
import { toggleWebAppBool } from "../config_storage/actions";
import { destroyAll } from "../api/crud";
import { success, error } from "farmbot-toastr";
import { Feature } from "../devices/interfaces";
/** First-party Farmware settings. */
export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
const listBtnColor = props.show ? "green" : "red";
return <div>
return <div className="farmware-settings-menu-contents">
<label>
{t("First-party Farmware")}
</label>
@ -33,5 +36,16 @@ export function FarmwareConfigMenu(props: FarmwareConfigMenuProps) {
onClick={() =>
props.dispatch(toggleWebAppBool("show_first_party_farmware"))} />
</fieldset>
{props.shouldDisplay(Feature.api_farmware_env) &&
<fieldset>
<label>
{t("Delete all Farmware data")}
</label>
<button
className={"fb-button red fa fa-trash"}
onClick={() => destroyAll("FarmwareEnv")
.then(() => success(t("Farmware data successfully deleted.")))
.catch(() => error(t("Error deleting Farmware data")))} />
</fieldset>}
</div>;
}

View File

@ -9,6 +9,7 @@ import { FarmwareConfigMenu } from "./farmware_config_menu";
import { every, Dictionary } from "lodash";
import { Popover, Position } from "@blueprintjs/core";
import { Link } from "../link";
import { ShouldDisplay } from "../devices/interfaces";
const DISPLAY_NAMES: Dictionary<string> = {
"Photos": t("Photos"),
@ -45,6 +46,7 @@ export interface FarmwareListProps {
farmwares: Farmwares;
showFirstParty: boolean;
firstPartyFarmwareNames: string[];
shouldDisplay: ShouldDisplay;
}
interface FarmwareListState {
@ -94,6 +96,7 @@ export class FarmwareList
<i className="fa fa-gear dark" />
<FarmwareConfigMenu
show={this.props.showFirstParty}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch}
firstPartyFwsInstalled={
this.firstPartyFarmwaresPresent(

View File

@ -159,6 +159,7 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
<FarmwareList
current={this.current}
dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay}
farmwares={this.props.farmwares}
firstPartyFarmwareNames={this.props.firstPartyFarmwareNames}
showFirstParty={!!this.props.webAppConfig.show_first_party_farmware} />

View File

@ -1,6 +1,7 @@
import { Dictionary, FarmwareManifest, SyncStatus } from "farmbot/dist";
import { NetworkState } from "../connectivity/interfaces";
import { BooleanConfigKey } from "farmbot/dist/resources/configs/web_app";
import { ShouldDisplay } from "../devices/interfaces";
export interface FWState {
selectedFarmware: string | undefined;
@ -28,6 +29,7 @@ export interface FarmwareConfigMenuProps {
show: boolean | undefined;
dispatch: Function;
firstPartyFwsInstalled: boolean;
shouldDisplay: ShouldDisplay;
}
export type Farmwares = Dictionary<FarmwareManifest | undefined>;