refactor external urls

pull/1698/head
gabrielburnworth 2020-02-15 10:30:23 -08:00
parent cf0af59e42
commit 66b5e3c962
29 changed files with 152 additions and 74 deletions

View File

@ -0,0 +1,33 @@
jest.unmock("../external_urls");
import { ExternalUrl } from "../external_urls";
/* tslint:disable:max-line-length */
describe("ExternalUrl", () => {
it("returns urls", () => {
expect(ExternalUrl.featureMinVersions)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json");
expect(ExternalUrl.osReleaseNotes)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md");
expect(ExternalUrl.latestRelease)
.toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest");
expect(ExternalUrl.webAppRepo)
.toEqual("https://github.com/FarmBot/Farmbot-Web-App");
expect(ExternalUrl.gitHubFarmBot)
.toEqual("https://github.com/FarmBot");
expect(ExternalUrl.softwareDocs)
.toEqual("https://software.farm.bot/docs");
expect(ExternalUrl.softwareForum)
.toEqual("http://forum.farmbot.org/c/software");
expect(ExternalUrl.OpenFarm.cropApi)
.toEqual("https://openfarm.cc/api/v1/crops/");
expect(ExternalUrl.OpenFarm.cropBrowse)
.toEqual("https://openfarm.cc/crops/");
expect(ExternalUrl.OpenFarm.newCrop)
.toEqual("https://openfarm.cc/en/crops/new");
expect(ExternalUrl.Videos.desktop)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018");
expect(ExternalUrl.Videos.mobile)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097");
});
});

View File

@ -158,6 +158,10 @@ export class API {
get farmwareInstallationPath() {
return `${this.baseUrl}/api/farmware_installations/`;
}
/** /api/first_party_farmwares */
get firstPartyFarmwarePath() {
return `${this.baseUrl}/api/first_party_farmwares`;
}
/** /api/alerts/:id */
get alertPath() { return `${this.baseUrl}/api/alerts/`; }
/** /api/global_bulletins/:id */

View File

@ -1,5 +1,6 @@
import * as React from "react";
import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
const OUTER_STYLE: React.CSSProperties = {
borderRadius: "10px",
@ -47,7 +48,7 @@ export function Apology(_: {}) {
<li>
<span>
Send a report to our developer team via the&nbsp;
<a href="http://forum.farmbot.org/c/software">FarmBot software
<a href={ExternalUrl.softwareForum}>FarmBot software
forum</a>. Including additional information (such as steps leading up
to the error) helps us identify solutions more quickly.
</span>

View File

@ -1,6 +1,6 @@
import axios from "axios";
import {
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL,
fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease
} from "../devices/actions";
import { AuthState } from "./interfaces";
@ -16,6 +16,7 @@ import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss);
@ -24,7 +25,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
beta_os_update_server && beta_os_update_server != "NOT_SET" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL));
dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions));
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState));

View File

@ -952,8 +952,7 @@ export namespace DiagnosticMessages {
but we have no recent record of FarmBot connecting to the internet.
This usually happens because of poor WiFi connectivity in the garden,
a bad password during configuration, a very long power outage, or
blocked ports on FarmBot's local network. Please refer IT staff to
https://software.farm.bot/docs/for-it-security-professionals`);
blocked ports on FarmBot's local network. Please refer IT staff to:`);
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
browser that does not support WebSockets, or are behind a firewall that

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { get } from "lodash";
import { Page } from "./ui/index";
import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
/** Use currying to pass down `error` object for now. */
export function crashPage(error: object) {
@ -24,7 +25,7 @@ export function crashPage(error: object) {
<li>Perform a "hard refresh" (<strong>CTRL + SHIFT + R</strong> on most machines).</li>
<li><span><a onClick={() => Session.clear()}>Log out by clicking here.</a></span></li>
<li>Send the error information (below) to our developer team via the
<a href="http://forum.farmbot.org/c/software">FarmBot software
<a href={ExternalUrl.softwareForum}>FarmBot software
forum</a>. Including additional information (such as steps leading up
to the error) help us identify solutions more quickly. </li>
</ol>

View File

@ -2,16 +2,13 @@ import { connect, MqttClient } from "mqtt";
import React from "react";
import { uuid } from "farmbot";
import axios from "axios";
import { ExternalUrl } from "../external_urls";
interface State {
error: Error | undefined;
stage: string;
}
const VIDEO_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018";
const PHONE_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097";
const WS_CONFIG = {
username: "farmbot_demo",
password: "required, but not used.",
@ -63,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> {
return <div className="demo-container">
<video muted={true} autoPlay={true} loop={true} className="demo-video">
<source src={VIDEO_URL} type="video/mp4" />
<source src={ExternalUrl.Videos.desktop} type="video/mp4" />
</video>
<img className="demo-phone" src={PHONE_URL} />
<img className="demo-phone" src={ExternalUrl.Videos.mobile} />
<button className="demo-button" onClick={this.requestAccount}>
{this.state.stage}
</button>

View File

@ -26,9 +26,6 @@ import { t } from "../i18next_wrapper";
const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams;
export const FEATURE_MIN_VERSIONS_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
"FEATURE_MIN_VERSIONS.json";
// Already filtering messages in FarmBot OS and the API- this is just for
// an additional layer of safety.
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];

View File

@ -15,6 +15,7 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls";
export enum ColWidth {
label = 3,
@ -22,15 +23,12 @@ export enum ColWidth {
button = 2
}
const OS_RELEASE_NOTES_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md";
export class FarmbotOsSettings
extends React.Component<FarmbotOsProps, FarmbotOsState> {
state: FarmbotOsState = { allOsReleaseNotes: "" };
componentDidMount() {
this.fetchReleaseNotes(OS_RELEASE_NOTES_URL);
this.fetchReleaseNotes(ExternalUrl.osReleaseNotes);
}
get osMajorVersion() {

View File

@ -14,6 +14,7 @@ import { timeFormatString } from "../../../util";
import { TimeSettings } from "../../../interfaces";
import { StringConfigKey } from "farmbot/dist/resources/configs/fbos";
import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
import { ExternalUrl, FarmBotRepo } from "../../../external_urls";
/** Return an indicator color for the given temperature (C). */
export const colorFromTemp = (temp: number | undefined): string => {
@ -170,7 +171,7 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8);
interface CommitDisplayProps {
title: string;
repo: string;
repo: FarmBotRepo;
commit: string;
}
@ -184,7 +185,7 @@ const CommitDisplay = (
{shortCommit === "---"
? shortCommit
: <a
href={`https://github.com/FarmBot/${repo}/tree/${shortCommit}`}
href={`${ExternalUrl.gitHubFarmBot}/${repo}/tree/${shortCommit}`}
target="_blank">
{shortCommit}
</a>}
@ -270,14 +271,15 @@ export function FbosDetails(props: FbosDetailsProps) {
timeSettings={props.timeSettings}
device={props.deviceAccount} />
<p><b>{t("Environment")}: </b>{env}</p>
<CommitDisplay title={t("Commit")} repo={"farmbot_os"} commit={commit} />
<CommitDisplay title={t("Commit")}
repo={FarmBotRepo.FarmBotOS} commit={commit} />
<p><b>{t("Target")}: </b>{target}</p>
<p><b>{t("Node name")}: </b>{last((node_name || "").split("@"))}</p>
<p><b>{t("Device ID")}: </b>{props.deviceAccount.body.id}</p>
{isString(private_ip) && <p><b>{t("Local IP address")}: </b>{private_ip}</p>}
<p><b>{t("Firmware")}: </b>{reformatFwVersion(firmware_version)}</p>
<CommitDisplay title={t("Firmware commit")}
repo={"farmbot-arduino-firmware"} commit={firmwareCommit} />
repo={FarmBotRepo.FarmBotArduinoFirmware} commit={firmwareCommit} />
<p><b>{t("Firmware code")}: </b>{firmware_version}</p>
{isNumber(uptime) && <UptimeDisplay uptime_sec={uptime} />}
{isNumber(memory_usage) &&

View File

@ -1,5 +1,11 @@
import { Dictionary } from "farmbot";
import { DiagnosticMessages } from "../../constants";
import { docLink } from "../../ui/doc_link";
import { trim } from "../../util/util";
const DiagnosticMessagesWiFiOrConfig =
trim(`${DiagnosticMessages.WIFI_OR_CONFIG}
${docLink("for-it-security-professionals")}`);
// I don't like this at all.
// If anyone has a cleaner solution, I'd love to hear it.
@ -16,13 +22,13 @@ export const TRUTH_TABLE: Readonly<Dictionary<string | undefined>> = {
// 17: No MQTT connections.
[0b10001]: DiagnosticMessages.NO_WS_AVAILABLE,
// 24: Browser is connected to API and MQTT.
[0b11000]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b11000]: DiagnosticMessagesWiFiOrConfig,
// 9: At least the browser is connected to MQTT.
[0b01001]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b01001]: DiagnosticMessagesWiFiOrConfig,
// 8: At least the browser is connected to MQTT.
[0b01000]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b01000]: DiagnosticMessagesWiFiOrConfig,
// 25: Farmbot offline.
[0b11001]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b11001]: DiagnosticMessagesWiFiOrConfig,
// 2: Browser offline. Farmbot last seen by the API recently.
[0b00010]: DiagnosticMessages.NO_WS_AVAILABLE,
// 18: Farmbot last seen by the API recently.

View File

@ -93,7 +93,7 @@ export enum Feature {
variables = "variables",
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
/** Object fetched from ExternalUrl.featureMinVersions. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
export interface BotState {

View File

@ -0,0 +1,51 @@
enum Org {
FarmBot = "FarmBot",
FarmBotLabs = "FarmBot-Labs",
}
export enum FarmBotRepo {
FarmBotWebApp = "Farmbot-Web-App",
FarmBotOS = "farmbot_os",
FarmBotArduinoFirmware = "farmbot-arduino-firmware",
}
enum FbosFile {
featureMinVersions = "FEATURE_MIN_VERSIONS.json",
osReleaseNotes = "RELEASE_NOTES.md",
}
export namespace ExternalUrl {
const GITHUB = "https://github.com";
const GITHUB_RAW = "https://raw.githubusercontent.com";
const GITHUB_API = "https://api.github.com";
const OPENFARM = "https://openfarm.cc";
const SOFTWARE_DOCS = "https://software.farm.bot";
const FORUM = "http://forum.farmbot.org";
const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files";
const FBOS_RAW = `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}`;
export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`;
export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`;
export const latestRelease =
`${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`;
export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`;
export const webAppRepo =
`${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`;
export const softwareDocs = `${SOFTWARE_DOCS}/docs`;
export const softwareForum = `${FORUM}/c/software`;
export namespace OpenFarm {
export const cropApi = `${OPENFARM}/api/v1/crops/`;
export const cropBrowse = `${OPENFARM}/crops/`;
export const newCrop = `${OPENFARM}/en/crops/new`;
}
export namespace Videos {
export const desktop =
`${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`;
export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`;
}
}

View File

@ -67,9 +67,6 @@ export namespace OpenFarm {
type: string;
attributes: ImageAttrs;
}
export const cropUrl = "https://openfarm.cc/api/v1/crops";
export const browsingCropUrl = "https://openfarm.cc/crops/";
}
/** Returned by https://openfarm.cc/api/v1/crops?filter=q */
export interface CropSearchResult {

View File

@ -24,6 +24,7 @@ import {
import { startCase, isArray, chain, isNumber } from "lodash";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
import { ExternalUrl } from "../../external_urls";
interface InfoFieldProps {
title: string;
@ -170,7 +171,7 @@ const CropDragInfoTile =
const EditOnOpenFarm = ({ slug }: { slug: string }) =>
<div className="edit-on-openfarm">
<span>{t("Edit on")}&nbsp;</span>
<a href={OpenFarm.browsingCropUrl + slug} target="_blank"
<a href={ExternalUrl.OpenFarm.cropBrowse + slug} target="_blank"
title={t("Open OpenFarm.cc in a new tab")}>
{"OpenFarm"}
</a>

View File

@ -5,6 +5,7 @@ import {
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import { t } from "../../i18next_wrapper";
import { ExternalUrl } from "../../external_urls";
/** A stripped down version of OFSearchResult */
interface Result {
@ -24,7 +25,7 @@ export class OpenFarmResults extends React.Component<SearchResultProps, {}> {
get text(): JSX.Element {
return <p>{`${t(Content.CROP_NOT_FOUND_INTRO)} `}
<a href="https://openfarm.cc/en/crops/new" target="_blank">
<a href={ExternalUrl.OpenFarm.newCrop} target="_blank">
{t(Content.CROP_NOT_FOUND_LINK)}
</a>
</p>;

View File

@ -4,8 +4,10 @@ import { DEFAULT_ICON } from "../open_farm/icons";
import { Actions } from "../constants";
import { ExecutableType } from "farmbot/dist/resources/api_resources";
import { get } from "lodash";
import { ExternalUrl } from "../external_urls";
const url = (q: string) => `${OpenFarm.cropUrl}?include=pictures&filter=${q}`;
const url = (q: string) =>
`${ExternalUrl.OpenFarm.cropApi}?include=pictures&filter=${q}`;
const openFarmSearchQuery = (q: string): AxiosPromise<CropSearchResult> =>
axios.get<CropSearchResult>(url(q));

View File

@ -2,8 +2,8 @@ jest.mock("axios", () => ({
get: jest.fn(() => {
return Promise.resolve({
data: [
{ manifest: "url", name: "farmware0" },
{ manifest: "url", name: "farmware1" }
{ package: "farmware0" },
{ package: "farmware1" }
]
});
}),

View File

@ -1,17 +1,14 @@
import axios from "axios";
import { FarmwareManifestEntry } from "./interfaces";
import { Actions } from "../constants";
import { urlFor } from "../api/crud";
const farmwareManifestUrl =
"https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests" +
"/master/manifest.json";
import { API } from "../api";
import { FarmwareManifest } from "farmbot";
export const getFirstPartyFarmwareList = () => {
return (dispatch: Function) => {
axios.get<FarmwareManifestEntry[]>(farmwareManifestUrl)
axios.get<FarmwareManifest[]>(API.current.firstPartyFarmwarePath)
.then(r => {
const names = r.data.map((fw: FarmwareManifestEntry) => fw.name);
const names = r.data.map(fw => fw.package);
dispatch({
type: Actions.FETCH_FIRST_PARTY_FARMWARE_NAMES_OK,
payload: names

View File

@ -24,8 +24,6 @@ export interface FarmwareState {
infoOpen: boolean;
}
export type FarmwareManifestEntry = Record<"name" | "manifest", string>;
export interface FarmwareConfigMenuProps {
show: boolean | undefined;
dispatch: Function;

View File

@ -1,6 +1,5 @@
import * as React from "react";
const VIDEO_URL = "https://cdn.shopify.com/s/files/1/2040/0289/files/" +
"Farm_Designer_Loop.mp4?9552037556691879018";
import { ExternalUrl } from "../external_urls";
export const LaptopSplash = ({ className }: { className: string }) =>
<div className={className}>
@ -8,7 +7,7 @@ export const LaptopSplash = ({ className }: { className: string }) =>
<div className="laptop">
<div className="laptop-screen">
<video muted autoPlay loop>
<source src={VIDEO_URL} type="video/mp4" />
<source src={ExternalUrl.Videos.desktop} type="video/mp4" />
</video>
<span className="laptop-shine" />
</div>

View File

@ -3,6 +3,7 @@ import { AccountMenuProps } from "./interfaces";
import { Link } from "../link";
import { shortRevision } from "../util";
import { t } from "../i18next_wrapper";
import { ExternalUrl } from "../external_urls";
export const AdditionalMenu = (props: AccountMenuProps) => {
return <div className="nav-additional-menu">
@ -30,7 +31,7 @@ export const AdditionalMenu = (props: AccountMenuProps) => {
</div>
<div className="app-version">
<label>{t("VERSION")}</label>:&nbsp;
<a href="https://github.com/FarmBot/Farmbot-Web-App" target="_blank">
<a href={ExternalUrl.webAppRepo} target="_blank">
{shortRevision().slice(0, 8)}
</a>
</div>

View File

@ -1,10 +1,4 @@
import { OpenFarmAPI, svgToUrl } from "../icons";
describe("OpenFarmAPI", () => {
it("has a base URL", () => {
expect(OpenFarmAPI.OFBaseURL).toContain("openfarm.cc");
});
});
import { svgToUrl } from "../icons";
describe("svgToUrl()", () => {
it("returns svg url", () => {

View File

@ -1,7 +1,8 @@
import axios, { AxiosResponse } from "axios";
import { Dictionary } from "farmbot";
import { isObject } from "lodash";
import { OFCropAttrs, OFCropResponse, OpenFarmAPI, svgToUrl } from "./icons";
import { OFCropAttrs, OFCropResponse, svgToUrl } from "./icons";
import { ExternalUrl } from "../external_urls";
export type OFIcon = Readonly<OFCropAttrs>;
type IconDictionary = Dictionary<OFIcon | undefined>;
@ -57,7 +58,7 @@ const cacheTheIcon = (slug: string) =>
};
function HTTPIconFetch(slug: string) {
const url = OpenFarmAPI.OFBaseURL + slug;
const url = ExternalUrl.OpenFarm.cropApi + slug;
// Avoid duplicate requests.
if (promiseCache[url]) { return promiseCache[url]; }
promiseCache[url] = axios

View File

@ -1,4 +1,3 @@
const BASE = "https://openfarm.cc/api/v1/crops/";
export const DATA_URI = "data:image/svg+xml;utf8,";
export const DEFAULT_ICON = "/app-resources/img/generic-plant.svg";
@ -20,10 +19,6 @@ export interface OFCropResponse {
};
}
export namespace OpenFarmAPI {
export const OFBaseURL = BASE;
}
export function svgToUrl(xml: string | undefined): string {
return xml ?
(DATA_URI + encodeURIComponent(xml)) : DEFAULT_ICON;

View File

@ -3,9 +3,7 @@ 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";
import { ExternalUrl } from "../external_urls";
interface OsDownloadState {
tagName: string;
@ -49,7 +47,7 @@ export class OsDownload extends React.Component<{}, OsDownloadState> {
}
fetchLatestRelease = () =>
axios.get<GithubRelease>(LATEST_RELEASE_URL)
axios.get<GithubRelease>(ExternalUrl.latestRelease)
.then(resp =>
this.setState({
tagName: resp.data.tag_name,

View File

@ -8,6 +8,7 @@ import { API } from "../api";
import { Row, Col, Widget, WidgetHeader, WidgetBody } from "../ui";
import { TermsCheckbox } from "../front_page/terms_checkbox";
import { t } from "../i18next_wrapper";
import { ExternalUrl } from "../external_urls";
interface Props { }
interface State {
@ -86,7 +87,7 @@ export class TosUpdate extends React.Component<Props, Partial<State>> {
</p>
<p>
{t("Please send us an email at contact@farm.bot or see the ")}
<a href="http://forum.farmbot.org/">
<a href={ExternalUrl.softwareForum}>
{t("FarmBot forum.")}
</a>
</p>

View File

@ -1,8 +1,9 @@
import { docLink, BASE_URL } from "../doc_link";
import { docLink } from "../doc_link";
import { ExternalUrl } from "../../external_urls";
describe("docLink", () => {
it("creates doc links", () => {
expect(docLink()).toEqual(BASE_URL);
expect(docLink("farmware")).toEqual(BASE_URL + "farmware");
expect(docLink()).toEqual(ExternalUrl.softwareDocs + "/");
expect(docLink("farmware")).toEqual(ExternalUrl.softwareDocs + "/farmware");
});
});

View File

@ -1,4 +1,4 @@
export const BASE_URL = "https://software.farm.bot/docs/";
import { ExternalUrl } from "../external_urls";
/** A centralized list of all documentation slugs in the app makes it easier to
* rename / move links in the future. */
@ -7,11 +7,13 @@ export const DOC_SLUGS = {
"camera-calibration": "Camera Calibration",
"the-farmbot-web-app": "Web App",
"farmware": "Farmware",
"connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet"
"connecting-farmbot-to-the-internet": "Connecting FarmBot to the Internet",
"for-it-security-professionals": "For IT Security Professionals",
};
export type DocSlug = keyof typeof DOC_SLUGS;
/** WHY?: The function keeps things DRY. It also makes life easier when the
* documentation URL / slug name changes. */
export const docLink = (slug?: DocSlug) => BASE_URL + (slug || "");
export const docLink = (slug?: DocSlug) =>
`${ExternalUrl.softwareDocs}/${slug || ""}`;