photo page upload progress and refactoring
parent
810785d12e
commit
b28362ab82
|
@ -74,7 +74,7 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.image-metadatas {
|
||||
.image-metadata {
|
||||
display: flex;
|
||||
label {
|
||||
margin-left: 1rem;
|
||||
|
@ -89,6 +89,14 @@
|
|||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
.farmware-button {
|
||||
p {
|
||||
float: right;
|
||||
margin-top: 0.75rem;
|
||||
margin-right: 1rem;
|
||||
color: $medium_gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.index-indicator {
|
||||
|
|
|
@ -37,7 +37,7 @@ const isWorking = (job: JobProgress | undefined) =>
|
|||
job && (job.status == "working");
|
||||
|
||||
/** FBOS update download progress. */
|
||||
function downloadProgress(job: JobProgress | undefined) {
|
||||
export function downloadProgress(job: JobProgress | undefined) {
|
||||
if (job && isWorking(job)) {
|
||||
switch (job.unit) {
|
||||
case "bytes":
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
TaggedSensor,
|
||||
TaggedDiagnosticDump,
|
||||
TaggedUser,
|
||||
TaggedFarmwareInstallation
|
||||
TaggedFarmwareInstallation,
|
||||
JobProgress,
|
||||
} from "farmbot";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces";
|
||||
|
@ -223,6 +224,7 @@ export interface FarmwareProps {
|
|||
shouldDisplay: ShouldDisplay;
|
||||
saveFarmwareEnv: SaveFarmwareEnv;
|
||||
taggedFarmwareInstallations: TaggedFarmwareInstallation[];
|
||||
imageJobs: JobProgress[];
|
||||
}
|
||||
|
||||
export interface HardwareSettingsProps {
|
||||
|
|
|
@ -42,6 +42,7 @@ describe("<FarmwarePage />", () => {
|
|||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from "../../__test_support__/fake_state/resources";
|
||||
import { edit, initSave, save } from "../../api/crud";
|
||||
import { fakeFarmware } from "../../__test_support__/fake_farmwares";
|
||||
import { JobProgress } from "farmbot";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
|
||||
|
@ -79,6 +80,44 @@ describe("mapStateToProps()", () => {
|
|||
[botFarmwareName]: botFarmware
|
||||
});
|
||||
});
|
||||
|
||||
it("returns image upload job list", () => {
|
||||
const state = fakeState();
|
||||
state.bot.hardware.jobs = {
|
||||
"img1.png": {
|
||||
status: "working",
|
||||
percent: 20,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 18:13:21.167440Z",
|
||||
} as JobProgress,
|
||||
"FBOS_OTA": {
|
||||
status: "working",
|
||||
percent: 10,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 17:13:21.167440Z",
|
||||
} as JobProgress,
|
||||
"img2.png": {
|
||||
status: "working",
|
||||
percent: 10,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 19:13:21.167440Z",
|
||||
} as JobProgress,
|
||||
};
|
||||
const props = mapStateToProps(state);
|
||||
expect(props.imageJobs).toEqual([
|
||||
{
|
||||
status: "working",
|
||||
percent: 10,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 19:13:21.167440Z"
|
||||
},
|
||||
{
|
||||
status: "working",
|
||||
percent: 20,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 18:13:21.167440Z"
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveOrEditFarmwareEnv()", () => {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import "../../../__test_support__/unmock_i18next";
|
||||
import * as React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { ImageFlipper } from "../image_flipper";
|
||||
import { ImageFlipper, PLACEHOLDER_FARMBOT } from "../image_flipper";
|
||||
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
||||
import { TaggedImage } from "farmbot";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import { ImageFlipperProps } from "../interfaces";
|
||||
|
||||
describe("<ImageFlipper/>", () => {
|
||||
function prepareImages(data: TaggedImage[]): TaggedImage[] {
|
||||
|
@ -17,114 +18,104 @@ describe("<ImageFlipper/>", () => {
|
|||
return images;
|
||||
}
|
||||
|
||||
const fakeProps = (): ImageFlipperProps => ({
|
||||
images: prepareImages(fakeImages),
|
||||
currentImage: undefined,
|
||||
onFlip: jest.fn(),
|
||||
});
|
||||
|
||||
it("defaults to index 0 and flips up", () => {
|
||||
const onFlip = jest.fn();
|
||||
const currentImage = undefined;
|
||||
const images = prepareImages(fakeImages);
|
||||
const props = { images, currentImage, onFlip };
|
||||
const x = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
const x = shallow(<ImageFlipper {...p} />);
|
||||
const up = (x.instance() as ImageFlipper).go(1);
|
||||
up();
|
||||
expect(onFlip).toHaveBeenCalledWith(images[1].uuid);
|
||||
expect(p.onFlip).toHaveBeenCalledWith(p.images[1].uuid);
|
||||
});
|
||||
|
||||
it("flips down", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[1];
|
||||
const props = { images, currentImage, onFlip };
|
||||
const x = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.currentImage = p.images[1];
|
||||
const x = shallow(<ImageFlipper {...p} />);
|
||||
const down = (x.instance() as ImageFlipper).go(-1);
|
||||
down();
|
||||
expect(onFlip).toHaveBeenCalledWith(images[0].uuid);
|
||||
expect(p.onFlip).toHaveBeenCalledWith(p.images[0].uuid);
|
||||
});
|
||||
|
||||
it("stops at upper end", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[2];
|
||||
const props = { images, currentImage, onFlip };
|
||||
const x = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.currentImage = p.images[2];
|
||||
const x = shallow(<ImageFlipper {...p} />);
|
||||
const up = (x.instance() as ImageFlipper).go(1);
|
||||
up();
|
||||
expect(onFlip).not.toHaveBeenCalled();
|
||||
expect(p.onFlip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops at lower end", () => {
|
||||
const images = prepareImages(fakeImages);
|
||||
const props = {
|
||||
images,
|
||||
currentImage: images[0],
|
||||
onFlip: jest.fn()
|
||||
};
|
||||
const x = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.currentImage = p.images[0];
|
||||
const x = shallow(<ImageFlipper {...p} />);
|
||||
const down = (x.instance() as ImageFlipper).go(-1);
|
||||
down();
|
||||
expect(props.onFlip).not.toHaveBeenCalled();
|
||||
expect(p.onFlip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables flippers when no images", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages([]);
|
||||
const currentImage = undefined;
|
||||
const props = { images, currentImage, onFlip };
|
||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.images = prepareImages([]);
|
||||
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables flippers when only one image", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages([fakeImages[0]]);
|
||||
const currentImage = undefined;
|
||||
const props = { images, currentImage, onFlip };
|
||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.images = prepareImages([fakeImages[0]]);
|
||||
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||
expect(wrapper.find("button").first().props().disabled).toBeTruthy();
|
||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables next flipper on load", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = undefined;
|
||||
const props = { images, currentImage, onFlip };
|
||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
||||
const wrapper = shallow(<ImageFlipper {...fakeProps()} />);
|
||||
wrapper.update();
|
||||
expect(wrapper.find("button").first().props().disabled).toBeFalsy();
|
||||
expect(wrapper.find("button").last().props().disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables flipper at lower end", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[1];
|
||||
const props = { images, currentImage, onFlip };
|
||||
const wrapper = shallow(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.currentImage = p.images[1];
|
||||
const wrapper = shallow(<ImageFlipper {...p} />);
|
||||
wrapper.setState({ disableNext: false });
|
||||
const nextButton = wrapper.render().find("button").last();
|
||||
expect(nextButton.text().toLowerCase()).toBe("next");
|
||||
expect(nextButton.prop("disabled")).toBeFalsy();
|
||||
wrapper.find("button").last().simulate("click");
|
||||
expect(onFlip).toHaveBeenLastCalledWith(images[0].uuid);
|
||||
expect(p.onFlip).toHaveBeenLastCalledWith(p.images[0].uuid);
|
||||
expect(wrapper.find("button").last().render().prop("disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables flipper at upper end", () => {
|
||||
const onFlip = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[1];
|
||||
const props = { images, currentImage, onFlip };
|
||||
const wrapper = mount(<ImageFlipper {...props} />);
|
||||
const p = fakeProps();
|
||||
p.currentImage = p.images[1];
|
||||
const wrapper = mount(<ImageFlipper {...p} />);
|
||||
const prevButton = wrapper.find("button").first();
|
||||
expect(prevButton.text().toLowerCase()).toBe("prev");
|
||||
expect(prevButton.props().disabled).toBeFalsy();
|
||||
prevButton.simulate("click");
|
||||
wrapper.update();
|
||||
// FAILED
|
||||
expect(onFlip).toHaveBeenCalledWith(images[2].uuid);
|
||||
expect(p.onFlip).toHaveBeenCalledWith(p.images[2].uuid);
|
||||
expect(wrapper.find("button").first().render().prop("disabled")).toBeTruthy();
|
||||
prevButton.simulate("click");
|
||||
expect(onFlip).toHaveBeenCalledTimes(1);
|
||||
expect(p.onFlip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders placeholder", () => {
|
||||
const p = fakeProps();
|
||||
p.images[0].body.attachment_processed_at = undefined;
|
||||
p.currentImage = p.images[0];
|
||||
const wrapper = mount(<ImageFlipper {...p} />);
|
||||
expect(wrapper.find("img").last().props().src).toEqual(PLACEHOLDER_FARMBOT);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,60 +1,135 @@
|
|||
jest.mock("../../../api/crud", () => ({
|
||||
destroy: jest.fn(),
|
||||
}));
|
||||
const mockDevice = { takePhoto: jest.fn(() => Promise.resolve({})) };
|
||||
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
|
||||
|
||||
jest.mock("../../../api/crud", () => ({ destroy: jest.fn() }));
|
||||
|
||||
jest.mock("../actions", () => ({ selectImage: jest.fn() }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { Photos } from "../photos";
|
||||
import { TaggedImage } from "farmbot";
|
||||
import { JobProgress } from "farmbot";
|
||||
import { fakeImages } from "../../../__test_support__/fake_state/images";
|
||||
import { defensiveClone } from "../../../util";
|
||||
import { destroy } from "../../../api/crud";
|
||||
import { clickButton } from "../../../__test_support__/helpers";
|
||||
import { PhotosProps } from "../interfaces";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { selectImage } from "../actions";
|
||||
|
||||
describe("<Photos/>", () => {
|
||||
function prepareImages(data: TaggedImage[]): TaggedImage[] {
|
||||
const images: TaggedImage[] = [];
|
||||
data.forEach((item, index) => {
|
||||
const image = defensiveClone(item);
|
||||
image.uuid = `Position ${index}`;
|
||||
images.push(image);
|
||||
});
|
||||
return images;
|
||||
}
|
||||
const fakeProps = (): PhotosProps => ({
|
||||
images: [],
|
||||
currentImage: undefined,
|
||||
dispatch: jest.fn(),
|
||||
timeOffset: 0,
|
||||
imageJobs: [],
|
||||
});
|
||||
|
||||
it("shows photo", () => {
|
||||
const dispatch = jest.fn();
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[1];
|
||||
const props = { images, currentImage, dispatch, timeOffset: 0 };
|
||||
const wrapper = mount(<Photos {...props} />);
|
||||
const p = fakeProps();
|
||||
const images = fakeImages;
|
||||
p.currentImage = images[1];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
expect(wrapper.text()).toContain("Created At:June 1st, 2017");
|
||||
expect(wrapper.text()).toContain("X:632Y:347Z:164");
|
||||
});
|
||||
|
||||
it("no photos", () => {
|
||||
const props = {
|
||||
images: [],
|
||||
currentImage: undefined,
|
||||
dispatch: jest.fn(),
|
||||
timeOffset: 0
|
||||
};
|
||||
const wrapper = mount(<Photos {...props} />);
|
||||
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||
expect(wrapper.text()).toContain("Image:No meta data.");
|
||||
});
|
||||
|
||||
it("deletes photo", () => {
|
||||
const dispatch = jest.fn(() => { return Promise.resolve(); });
|
||||
const images = prepareImages(fakeImages);
|
||||
const currentImage = images[1];
|
||||
const props = {
|
||||
images,
|
||||
currentImage,
|
||||
dispatch,
|
||||
timeOffset: 0
|
||||
};
|
||||
const wrapper = mount(<Photos {...props} />);
|
||||
it("takes photo", async () => {
|
||||
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||
await clickButton(wrapper, 0, "take photo");
|
||||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
await expect(success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails to take photo", async () => {
|
||||
mockDevice.takePhoto = jest.fn(() => Promise.reject());
|
||||
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||
await clickButton(wrapper, 0, "take photo");
|
||||
expect(mockDevice.takePhoto).toHaveBeenCalled();
|
||||
await expect(error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes photo", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
const images = fakeImages;
|
||||
p.currentImage = images[1];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
await clickButton(wrapper, 1, "delete photo");
|
||||
expect(destroy).toHaveBeenCalledWith(p.currentImage.uuid);
|
||||
await expect(success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails to delete photo", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.reject("error"));
|
||||
const images = fakeImages;
|
||||
p.currentImage = images[1];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
await clickButton(wrapper, 1, "delete photo");
|
||||
await expect(destroy).toHaveBeenCalledWith(p.currentImage.uuid);
|
||||
await expect(error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes most recent photo", async () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn(() => Promise.resolve());
|
||||
p.images = fakeImages;
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
await clickButton(wrapper, 1, "delete photo");
|
||||
expect(destroy).toHaveBeenCalledWith(p.images[0].uuid);
|
||||
await expect(success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("no photos to delete", () => {
|
||||
const wrapper = mount(<Photos {...fakeProps()} />);
|
||||
clickButton(wrapper, 1, "delete photo");
|
||||
expect(destroy).toHaveBeenCalledWith("Position 1");
|
||||
expect(destroy).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("shows image download progress", () => {
|
||||
const p = fakeProps();
|
||||
p.imageJobs = [{
|
||||
status: "working",
|
||||
percent: 15,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 19:13:21.167440Z"
|
||||
} as JobProgress];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
expect(wrapper.text()).toContain("uploading photo...15%");
|
||||
});
|
||||
|
||||
it("doesn't show image download progress", () => {
|
||||
const p = fakeProps();
|
||||
p.imageJobs = [{
|
||||
status: "complete",
|
||||
percent: 15,
|
||||
unit: "percent",
|
||||
time: "2018-11-15 19:13:21.167440Z"
|
||||
} as JobProgress];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
expect(wrapper.text()).not.toContain("uploading");
|
||||
});
|
||||
|
||||
it("can't find meta field data", () => {
|
||||
const p = fakeProps();
|
||||
p.images = fakeImages;
|
||||
p.images[0].body.meta.x = undefined;
|
||||
p.currentImage = p.images[0];
|
||||
const wrapper = mount(<Photos {...p} />);
|
||||
expect(wrapper.text()).toContain("X:unknown");
|
||||
});
|
||||
|
||||
it("flips photo", () => {
|
||||
const p = fakeProps();
|
||||
p.images = fakeImages;
|
||||
const wrapper = shallow(<Photos {...p} />);
|
||||
wrapper.find("ImageFlipper").simulate("flip", 1);
|
||||
expect(selectImage).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,15 @@ import { Content } from "../../constants";
|
|||
|
||||
export const PLACEHOLDER_FARMBOT = "/placeholder_farmbot.jpg";
|
||||
|
||||
/** Placeholder image with text overlay. */
|
||||
const PlaceholderImg = ({ textOverlay }: { textOverlay: string }) =>
|
||||
<div className="no-flipper-image-container">
|
||||
<p>{t(textOverlay)}</p>
|
||||
<img
|
||||
className="image-flipper-image"
|
||||
src={PLACEHOLDER_FARMBOT} />
|
||||
</div>;
|
||||
|
||||
export class ImageFlipper extends
|
||||
React.Component<ImageFlipperProps, Partial<ImageFlipperState>> {
|
||||
|
||||
|
@ -16,30 +25,22 @@ export class ImageFlipper extends
|
|||
|
||||
imageJSX = () => {
|
||||
if (this.props.images.length > 0) {
|
||||
const i = this.props.currentImage || this.props.images[0];
|
||||
let url: string;
|
||||
url = (i.body.attachment_processed_at) ?
|
||||
i.body.attachment_url : PLACEHOLDER_FARMBOT;
|
||||
const image = this.props.currentImage || this.props.images[0];
|
||||
const url = image.body.attachment_processed_at
|
||||
? image.body.attachment_url
|
||||
: PLACEHOLDER_FARMBOT;
|
||||
return <div>
|
||||
{!this.state.isLoaded && (
|
||||
<div className="no-flipper-image-container">
|
||||
<p>{t(`Image loading (try refreshing)`)}</p>
|
||||
<img
|
||||
className="image-flipper-image"
|
||||
src={PLACEHOLDER_FARMBOT} />
|
||||
</div>)}
|
||||
{!this.state.isLoaded &&
|
||||
<PlaceholderImg
|
||||
textOverlay={t("Image loading (try refreshing)")} />}
|
||||
<img
|
||||
onLoad={() => this.setState({ isLoaded: true })}
|
||||
className={`image-flipper-image is-loaded-${this.state.isLoaded}`}
|
||||
src={url} />
|
||||
</div>;
|
||||
} else {
|
||||
return <div className="no-flipper-image-container">
|
||||
<p>{t(Content.NO_IMAGES_YET)}</p>
|
||||
<img
|
||||
className="image-flipper-image"
|
||||
src={PLACEHOLDER_FARMBOT} />
|
||||
</div>;
|
||||
return <PlaceholderImg
|
||||
textOverlay={Content.NO_IMAGES_YET} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,10 +62,9 @@ export class ImageFlipper extends
|
|||
}
|
||||
|
||||
render() {
|
||||
const image = this.imageJSX();
|
||||
const multipleImages = this.props.images.length > 1;
|
||||
return <div className="image-flipper">
|
||||
{image}
|
||||
<this.imageJSX />
|
||||
<button
|
||||
onClick={this.go(1)}
|
||||
disabled={!multipleImages || this.state.disablePrev}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TaggedImage } from "farmbot";
|
||||
import { TaggedImage, JobProgress } from "farmbot";
|
||||
|
||||
export interface ImageFlipperProps {
|
||||
onFlip(uuid: string | undefined): void;
|
||||
|
@ -17,4 +17,5 @@ export interface PhotosProps {
|
|||
images: TaggedImage[];
|
||||
currentImage: TaggedImage | undefined;
|
||||
timeOffset: number;
|
||||
imageJobs: JobProgress[];
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ import { Content } from "../../constants";
|
|||
import { selectImage } from "./actions";
|
||||
import { safeStringFetch } from "../../util";
|
||||
import { destroy } from "../../api/crud";
|
||||
import {
|
||||
downloadProgress
|
||||
} from "../../devices/components/fbos_settings/os_update_button";
|
||||
import { JobProgress, TaggedImage } from "farmbot";
|
||||
|
||||
interface MetaInfoProps {
|
||||
/** Default conversion is `attr_name ==> Attr Name`.
|
||||
|
@ -30,6 +34,67 @@ function MetaInfo({ obj, attr, label }: MetaInfoProps) {
|
|||
</div>;
|
||||
}
|
||||
|
||||
const PhotoMetaData = ({ image }: { image: TaggedImage | undefined }) =>
|
||||
<div className="image-metadata">
|
||||
{image
|
||||
? Object.keys(image.body.meta)
|
||||
.filter(key => ["x", "y", "z"].includes(key))
|
||||
.sort()
|
||||
.map((key, index) =>
|
||||
<MetaInfo key={index} attr={key} obj={image.body.meta} />)
|
||||
: <MetaInfo
|
||||
label={t("Image")}
|
||||
attr={"image"}
|
||||
obj={{ image: t("No meta data.") }} />}
|
||||
</div>;
|
||||
|
||||
const PhotoButtons = (props: {
|
||||
takePhoto: () => void,
|
||||
deletePhoto: () => void,
|
||||
imageJobs: JobProgress[]
|
||||
}) => {
|
||||
const imageUploadJobProgress = downloadProgress(props.imageJobs[0]);
|
||||
return <div className="farmware-button">
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={props.takePhoto}>
|
||||
{t("Take Photo")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={props.deletePhoto}>
|
||||
{t("Delete Photo")}
|
||||
</button>
|
||||
<p>
|
||||
{imageUploadJobProgress &&
|
||||
`${t("uploading photo")}...${imageUploadJobProgress}`}
|
||||
</p>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const PhotoFooter = ({ image, timeOffset }: {
|
||||
image: TaggedImage | undefined,
|
||||
timeOffset: number
|
||||
}) => {
|
||||
const created_at = image
|
||||
? moment(image.body.created_at)
|
||||
.utcOffset(timeOffset)
|
||||
.format("MMMM Do, YYYY h:mma")
|
||||
: "";
|
||||
return <div className="photos-footer">
|
||||
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
||||
{image ?
|
||||
<div className="image-created-at">
|
||||
<label>{t("Created At:")}</label>
|
||||
<span>
|
||||
{created_at}
|
||||
</span>
|
||||
</div>
|
||||
: ""}
|
||||
<PhotoMetaData image={image} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
export class Photos extends React.Component<PhotosProps, {}> {
|
||||
|
||||
takePhoto = () => {
|
||||
|
@ -38,22 +103,7 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
|||
getDevice().takePhoto().then(ok, no);
|
||||
}
|
||||
|
||||
metaDatas() {
|
||||
const i = this.props.currentImage;
|
||||
if (i) {
|
||||
const { meta } = i.body;
|
||||
return Object.keys(meta)
|
||||
.filter(key => ["x", "y", "z"].includes(key))
|
||||
.sort()
|
||||
.map((key, index) => {
|
||||
return <MetaInfo key={index} attr={key} obj={meta} />;
|
||||
});
|
||||
} else {
|
||||
return <MetaInfo attr={t("image")} obj={{ image: t("No meta data.") }} />;
|
||||
}
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
deletePhoto = () => {
|
||||
const img = this.props.currentImage || this.props.images[0];
|
||||
if (img && img.uuid) {
|
||||
this.props.dispatch(destroy(img.uuid))
|
||||
|
@ -63,43 +113,18 @@ export class Photos extends React.Component<PhotosProps, {}> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const image = this.props.currentImage;
|
||||
const created_at = image
|
||||
? moment(image.body.created_at)
|
||||
.utcOffset(this.props.timeOffset)
|
||||
.format("MMMM Do, YYYY h:mma")
|
||||
: "";
|
||||
return <div className="photos">
|
||||
<div className="farmware-button">
|
||||
<button
|
||||
className="fb-button green"
|
||||
onClick={this.takePhoto}>
|
||||
{t("Take Photo")}
|
||||
</button>
|
||||
<button
|
||||
className="fb-button red"
|
||||
onClick={() => this.destroy()}>
|
||||
{t("Delete Photo")}
|
||||
</button>
|
||||
</div>
|
||||
<PhotoButtons
|
||||
takePhoto={this.takePhoto}
|
||||
deletePhoto={this.deletePhoto}
|
||||
imageJobs={this.props.imageJobs} />
|
||||
<ImageFlipper
|
||||
onFlip={id => { this.props.dispatch(selectImage(id)); }}
|
||||
onFlip={id => this.props.dispatch(selectImage(id))}
|
||||
currentImage={this.props.currentImage}
|
||||
images={this.props.images} />
|
||||
<div className="photos-footer">
|
||||
{/** Separated from <MetaInfo /> for stylistic purposes. */}
|
||||
{image ?
|
||||
<div className="image-created-at">
|
||||
<label>{t("Created At:")}</label>
|
||||
<span>
|
||||
{created_at}
|
||||
</span>
|
||||
</div>
|
||||
: ""}
|
||||
<div className="image-metadatas">
|
||||
{this.metaDatas()}
|
||||
</div>
|
||||
</div>
|
||||
<PhotoFooter
|
||||
image={this.props.currentImage}
|
||||
timeOffset={this.props.timeOffset} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,7 +122,8 @@ export class FarmwarePage extends React.Component<FarmwareProps, {}> {
|
|||
timeOffset={this.props.timeOffset}
|
||||
dispatch={this.props.dispatch}
|
||||
images={this.props.images}
|
||||
currentImage={this.props.currentImage} />;
|
||||
currentImage={this.props.currentImage}
|
||||
imageJobs={this.props.imageJobs} />;
|
||||
case "camera_calibration":
|
||||
return <CameraCalibration
|
||||
syncStatus={this.props.syncStatus}
|
||||
|
|
|
@ -13,10 +13,11 @@ import {
|
|||
import {
|
||||
determineInstalledOsVersion,
|
||||
shouldDisplay as shouldDisplayFunc,
|
||||
trim
|
||||
trim,
|
||||
betterCompact
|
||||
} from "../util";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { TaggedFarmwareEnv, FarmwareManifest } from "farmbot";
|
||||
import { TaggedFarmwareEnv, FarmwareManifest, JobProgress } from "farmbot";
|
||||
import { save, edit, initSave } from "../api/crud";
|
||||
import { t } from "i18next";
|
||||
|
||||
|
@ -97,6 +98,14 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
|||
}
|
||||
});
|
||||
|
||||
const { jobs } = props.bot.hardware;
|
||||
const imageJobNames = Object.keys(jobs).filter(x => x != "FBOS_OTA");
|
||||
const imageJobs: JobProgress[] =
|
||||
_(betterCompact(imageJobNames.map(x => jobs[x])))
|
||||
.sortBy("time")
|
||||
.reverse()
|
||||
.value();
|
||||
|
||||
return {
|
||||
timeOffset: maybeGetTimeOffset(props.resources.index),
|
||||
currentFarmware,
|
||||
|
@ -113,5 +122,6 @@ export function mapStateToProps(props: Everything): FarmwareProps {
|
|||
shouldDisplay,
|
||||
saveFarmwareEnv: saveOrEditFarmwareEnv(props.resources.index),
|
||||
taggedFarmwareInstallations,
|
||||
imageJobs,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ describe("<WeedDetector />", () => {
|
|||
shouldDisplay: () => false,
|
||||
saveFarmwareEnv: jest.fn(),
|
||||
taggedFarmwareInstallations: [],
|
||||
imageJobs: [],
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
|
Loading…
Reference in New Issue