add os download page
parent
7e1cad0f9f
commit
9dfd31da10
|
@ -7,6 +7,7 @@ class DashboardController < ApplicationController
|
||||||
:main_app,
|
:main_app,
|
||||||
:password_reset,
|
:password_reset,
|
||||||
:tos_update,
|
:tos_update,
|
||||||
|
:os_download,
|
||||||
:demo]
|
:demo]
|
||||||
|
|
||||||
OUTPUT_URL = "/" + File.join("assets", "parcel") # <= served from public/ dir
|
OUTPUT_URL = "/" + File.join("assets", "parcel") # <= served from public/ dir
|
||||||
|
@ -24,6 +25,7 @@ class DashboardController < ApplicationController
|
||||||
password_reset: "/password_reset/index.tsx",
|
password_reset: "/password_reset/index.tsx",
|
||||||
tos_update: "/tos_update/index.tsx",
|
tos_update: "/tos_update/index.tsx",
|
||||||
demo: "/demo/index.tsx",
|
demo: "/demo/index.tsx",
|
||||||
|
os_download: "/os_download/index.tsx"
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
|
|
||||||
# === THESE CONSTANTS ARE NON-CONFIGURABLE. ===
|
# === 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" }
|
match "/app/*path", to: "dashboard#main_app", via: :all, constraints: { format: "html" }
|
||||||
|
|
||||||
get "/demo" => "dashboard#demo", as: :demo_main
|
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 "/password_reset/*token" => "dashboard#password_reset", as: :password_reset
|
||||||
get "/tos_update" => "dashboard#tos_update", as: :tos_update
|
get "/tos_update" => "dashboard#tos_update", as: :tos_update
|
||||||
get "/verify/:token" => "dashboard#confirmation_page", as: :confirmation_page
|
get "/verify/:token" => "dashboard#confirmation_page", as: :confirmation_page
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { store } from "../redux/store";
|
||||||
import { AuthState } from "../auth/interfaces";
|
import { AuthState } from "../auth/interfaces";
|
||||||
import { auth } from "../__test_support__/fake_state/token";
|
import { auth } from "../__test_support__/fake_state/token";
|
||||||
import { Session } from "../session";
|
import { Session } from "../session";
|
||||||
|
import { FourOhFour } from "../404";
|
||||||
|
|
||||||
describe("<RootComponent />", () => {
|
describe("<RootComponent />", () => {
|
||||||
it("clears session when not authorized", () => {
|
it("clears session when not authorized", () => {
|
||||||
|
@ -38,4 +39,10 @@ describe("<RootComponent />", () => {
|
||||||
expect(Session.clear).not.toHaveBeenCalled();
|
expect(Session.clear).not.toHaveBeenCalled();
|
||||||
wrapper.unmount();
|
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 =
|
export const NO_CAMERA_SELECTED =
|
||||||
trim(`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 {
|
export namespace TourContent {
|
||||||
|
|
|
@ -1528,3 +1528,72 @@ textarea:focus {
|
||||||
background: darken(lightgray, 10%);
|
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 {
|
export interface GithubRelease {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
target_commitish: string;
|
target_commitish: string;
|
||||||
|
assets: {
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OsUpdateInfo {
|
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)
|
expect(response.status).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "renders the os download page" do
|
||||||
|
get :os_download
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
it "renders the front page" do
|
it "renders the front page" do
|
||||||
get :front_page
|
get :front_page
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
|
|
Loading…
Reference in New Issue