add os download page
parent
7e1cad0f9f
commit
9dfd31da10
|
@ -7,6 +7,7 @@ class DashboardController < ApplicationController
|
|||
:main_app,
|
||||
:password_reset,
|
||||
:tos_update,
|
||||
:os_download,
|
||||
:demo]
|
||||
|
||||
OUTPUT_URL = "/" + File.join("assets", "parcel") # <= served from public/ dir
|
||||
|
@ -24,6 +25,7 @@ class DashboardController < ApplicationController
|
|||
password_reset: "/password_reset/index.tsx",
|
||||
tos_update: "/tos_update/index.tsx",
|
||||
demo: "/demo/index.tsx",
|
||||
os_download: "/os_download/index.tsx"
|
||||
}.with_indifferent_access
|
||||
|
||||
# === THESE CONSTANTS ARE NON-CONFIGURABLE. ===
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<%# Intentionally blank template required by router for content generated by frontend. %>
|
|
@ -115,6 +115,7 @@ FarmBot::Application.routes.draw do
|
|||
match "/app/*path", to: "dashboard#main_app", via: :all, constraints: { format: "html" }
|
||||
|
||||
get "/demo" => "dashboard#demo", as: :demo_main
|
||||
get "/os" => "dashboard#os_download", as: :os_download
|
||||
get "/password_reset/*token" => "dashboard#password_reset", as: :password_reset
|
||||
get "/tos_update" => "dashboard#tos_update", as: :tos_update
|
||||
get "/verify/:token" => "dashboard#confirmation_page", as: :confirmation_page
|
||||
|
|
|
@ -21,6 +21,7 @@ import { store } from "../redux/store";
|
|||
import { AuthState } from "../auth/interfaces";
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
import { Session } from "../session";
|
||||
import { FourOhFour } from "../404";
|
||||
|
||||
describe("<RootComponent />", () => {
|
||||
it("clears session when not authorized", () => {
|
||||
|
@ -38,4 +39,10 @@ describe("<RootComponent />", () => {
|
|||
expect(Session.clear).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("changes route", () => {
|
||||
const wrapper = shallow<RootComponent>(<RootComponent store={store} />);
|
||||
wrapper.instance().changeRoute(FourOhFour);
|
||||
expect(wrapper.state().Route).toEqual(FourOhFour);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -837,6 +837,11 @@ export namespace Content {
|
|||
|
||||
export const NO_CAMERA_SELECTED =
|
||||
trim(`No camera selected`);
|
||||
|
||||
// Other
|
||||
export const DOWNLOAD_FBOS =
|
||||
trim(`Download the version of FarmBot OS that corresponds to your
|
||||
FarmBot kit and its internal computer.`);
|
||||
}
|
||||
|
||||
export namespace TourContent {
|
||||
|
|
|
@ -1528,3 +1528,72 @@ textarea:focus {
|
|||
background: darken(lightgray, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.transparent-link-button {
|
||||
font-size: 1rem;
|
||||
border: 1px solid;
|
||||
padding: 0.4rem 1.2rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
border-radius: 4px;
|
||||
color: $off_white;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.os-download-page {
|
||||
text-align: center;
|
||||
.all-content-wrapper {
|
||||
padding-top: 0 !important;
|
||||
min-height: 30rem;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 5rem;
|
||||
font-size: 2rem !important;
|
||||
text-shadow: 0 0 5px rgba(0, 0, 0, 0.1), 0 0 25px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
p {
|
||||
margin: auto;
|
||||
width: 70%;
|
||||
color: $off_white;
|
||||
}
|
||||
a {
|
||||
white-space: nowrap;
|
||||
font-weight: bold !important;
|
||||
color: $off_white !important;
|
||||
&:hover {
|
||||
color: $white !important;
|
||||
}
|
||||
&:visited {
|
||||
color: $off_white;
|
||||
}
|
||||
&:link {
|
||||
color: $off_white;
|
||||
}
|
||||
&:active {
|
||||
color: $dark_gray !important;
|
||||
}
|
||||
}
|
||||
table {
|
||||
margin: 2rem;
|
||||
margin-top: 3rem;
|
||||
width: 93%;
|
||||
font-size: 1.2rem;
|
||||
color: $off_white;
|
||||
text-align: left;
|
||||
thead {
|
||||
border-bottom: 2px solid $off_white;
|
||||
}
|
||||
tr:not(:last-child) {
|
||||
border-bottom: 1px solid $gray;
|
||||
}
|
||||
td {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,10 @@ export type HardwareState = BotStateTree;
|
|||
export interface GithubRelease {
|
||||
tag_name: string;
|
||||
target_commitish: string;
|
||||
assets: {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OsUpdateInfo {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
const mockRelease = {
|
||||
tag_name: "v1.0.0",
|
||||
assets: [
|
||||
{ name: "farmbot-rpi-1.0.0.fw", browser_download_url: "fake rpi fw url" },
|
||||
{ name: "farmbot-rpi-1.0.0.img", browser_download_url: "fake rpi img url" },
|
||||
{ name: "farmbot-rpi3-1.0.0.img", browser_download_url: "fake rpi3 img url" },
|
||||
]
|
||||
};
|
||||
let mockResponse = Promise.resolve({ data: mockRelease });
|
||||
jest.mock("axios", () => ({ get: jest.fn(() => mockResponse) }));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { OsDownload } from "../content";
|
||||
|
||||
describe("<OsDownload />", () => {
|
||||
it("fetches and renders", async () => {
|
||||
const wrapper = await mount<OsDownload>(<OsDownload />);
|
||||
expect(wrapper.state().tagName).toEqual("v1.0.0");
|
||||
expect(wrapper.state().genesisImg).toEqual("fake rpi3 img url");
|
||||
expect(wrapper.text()).toContain("Download FBOS v1.0.0");
|
||||
wrapper.update();
|
||||
expect(wrapper.find("a").first().props().href)
|
||||
.toEqual("fake rpi3 img url");
|
||||
});
|
||||
|
||||
it("uses fallback", async () => {
|
||||
globalConfig.GENESIS_IMG_FALLBACK = "fake rpi3 img fallback url///////v0.0.0";
|
||||
mockResponse = Promise.reject();
|
||||
const wrapper = await mount(<OsDownload />);
|
||||
expect(wrapper.text()).toContain("Download FBOS v0.0.0");
|
||||
expect(wrapper.find("a").first().props().href)
|
||||
.toEqual(globalConfig.GENESIS_IMG_FALLBACK);
|
||||
});
|
||||
|
||||
it("uses override", async () => {
|
||||
globalConfig.GENESIS_IMG_OVERRIDE = "fake rpi3 img override url";
|
||||
const wrapper = await mount(<OsDownload />);
|
||||
expect(wrapper.text()).toContain("Download FBOS");
|
||||
wrapper.update();
|
||||
expect(wrapper.find("a").first().props().href)
|
||||
.toEqual(globalConfig.GENESIS_IMG_OVERRIDE);
|
||||
});
|
||||
|
||||
it("handles missing data", async () => {
|
||||
delete globalConfig.GENESIS_IMG_OVERRIDE;
|
||||
delete mockRelease.assets;
|
||||
const wrapper = await mount(<OsDownload />);
|
||||
wrapper.update();
|
||||
expect(wrapper.find("a").first().props().href)
|
||||
.toEqual(globalConfig.GENESIS_IMG_FALLBACK);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
jest.mock("i18next", () => ({ init: jest.fn((_, ok) => ok()) }));
|
||||
jest.mock("react-dom", () => ({ render: jest.fn() }));
|
||||
jest.mock("../../i18n",
|
||||
() => ({ detectLanguage: jest.fn(() => Promise.resolve()) }));
|
||||
|
||||
import { detectLanguage } from "../../i18n";
|
||||
import { render } from "react-dom";
|
||||
|
||||
describe("index.ts", () => {
|
||||
it("attaches the os download page to the DOM", async () => {
|
||||
await import("../index");
|
||||
expect(detectLanguage).toHaveBeenCalled();
|
||||
expect(document.getElementById("root")).toBeTruthy();
|
||||
expect(render).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
import * as React from "react";
|
||||
import axios from "axios";
|
||||
import { t } from "../i18next_wrapper";
|
||||
import { GithubRelease } from "../devices/interfaces";
|
||||
import { Content } from "../constants";
|
||||
|
||||
const LATEST_RELEASE_URL =
|
||||
"https://api.github.com/repos/farmbot/farmbot_os/releases/latest";
|
||||
|
||||
interface OsDownloadState {
|
||||
tagName: string;
|
||||
genesisImg: string;
|
||||
expressImg: string;
|
||||
}
|
||||
|
||||
const getImgLink = (assets: GithubRelease["assets"], target: string) =>
|
||||
assets.filter(asset => asset.name.includes("img")
|
||||
&& asset.name.includes(target))[0]?.browser_download_url || "";
|
||||
|
||||
const tagNameFromUrl = (url: string) => {
|
||||
const tagPart = url.split("/")[7] || "";
|
||||
return tagPart.startsWith("v") ? tagPart : "";
|
||||
};
|
||||
|
||||
export class OsDownload extends React.Component<{}, OsDownloadState> {
|
||||
state: OsDownloadState = { tagName: "", genesisImg: "", expressImg: "" };
|
||||
|
||||
get genesisTagName() {
|
||||
return this.state.tagName || tagNameFromUrl(this.genesisImgDownloadLink);
|
||||
}
|
||||
|
||||
get expressTagName() {
|
||||
return this.state.tagName || tagNameFromUrl(this.expressImgDownloadLink);
|
||||
}
|
||||
|
||||
get genesisImgDownloadLink() {
|
||||
return globalConfig.GENESIS_IMG_OVERRIDE ||
|
||||
this.state.genesisImg ||
|
||||
globalConfig.GENESIS_IMG_FALLBACK || "";
|
||||
}
|
||||
|
||||
get expressImgDownloadLink() {
|
||||
return globalConfig.EXPRESS_IMG_OVERRIDE ||
|
||||
this.state.expressImg ||
|
||||
globalConfig.EXPRESS_IMG_FALLBACK || "";
|
||||
}
|
||||
|
||||
fetchLatestRelease = () =>
|
||||
axios.get<GithubRelease>(LATEST_RELEASE_URL)
|
||||
.then(resp =>
|
||||
this.setState({
|
||||
tagName: resp.data.tag_name,
|
||||
genesisImg: getImgLink(resp.data.assets, "rpi3"),
|
||||
expressImg: getImgLink(resp.data.assets, "rpi-"),
|
||||
})).catch(() => { });
|
||||
|
||||
componentDidMount() { this.fetchLatestRelease(); }
|
||||
|
||||
render() {
|
||||
return <div className="static-page os-download-page">
|
||||
<div className="all-content-wrapper">
|
||||
<h1>{t("Download FarmBot OS")}</h1>
|
||||
<p>{t(Content.DOWNLOAD_FBOS)}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("FarmBot Kit")}</th>
|
||||
<th>{t("Internal Computer")}</th>
|
||||
<th>{t("Download Link")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{"Genesis v1.2"}</span>
|
||||
<span>{"Genesis v1.3"}</span>
|
||||
<span>{"Genesis v1.4"}</span>
|
||||
<span>{"Genesis XL v1.4"}</span>
|
||||
<span>{"Genesis v1.5"}</span>
|
||||
<span>{"Genesis XL v1.5"}</span>
|
||||
</td>
|
||||
<td>{t("Raspberry Pi 3")}</td>
|
||||
<td>
|
||||
<a className="transparent-link-button"
|
||||
href={this.genesisImgDownloadLink}>
|
||||
{`${t("Download")} FBOS ${this.genesisTagName}`}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{"Express v1.0"}</span>
|
||||
<span>{"Express XL v1.0"}</span>
|
||||
</td>
|
||||
<td>{t("Raspberry Pi Zero W")}</td>
|
||||
<td>
|
||||
<a className="transparent-link-button"
|
||||
href={this.expressImgDownloadLink}>
|
||||
{`${t("Download")} FBOS ${this.expressTagName}`}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { render } from "react-dom";
|
||||
import I from "i18next";
|
||||
import { detectLanguage } from "../i18n";
|
||||
import * as _React from "react";
|
||||
import { createElement } from "react";
|
||||
import { OsDownload } from "./content";
|
||||
|
||||
const node = document.createElement("DIV");
|
||||
node.id = "root";
|
||||
document.body.appendChild(node);
|
||||
const domElem = document.getElementById("root");
|
||||
const reactElem = createElement(OsDownload, {});
|
||||
|
||||
const ok = () => domElem && render(reactElem, domElem);
|
||||
|
||||
detectLanguage().then(conf => I.init(conf, ok));
|
|
@ -10,6 +10,11 @@ describe DashboardController do
|
|||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "renders the os download page" do
|
||||
get :os_download
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "renders the front page" do
|
||||
get :front_page
|
||||
expect(response.status).to eq(200)
|
||||
|
|
Loading…
Reference in New Issue