add os download page

pull/1672/head
gabrielburnworth 2020-01-28 11:30:04 -08:00
parent 7e1cad0f9f
commit 9dfd31da10
12 changed files with 287 additions and 0 deletions

View File

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

View File

@ -0,0 +1 @@
<%# Intentionally blank template required by router for content generated by frontend. %>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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