mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-27 18:57:35 +00:00
wip
This commit is contained in:
parent
c27aa9e7ff
commit
c7c59d910c
@ -13,6 +13,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"swr": "^2.2.4",
|
||||||
"wouter": "^2.11.0"
|
"wouter": "^2.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -11,14 +11,12 @@ let unraidCsrfToken: string | undefined // required for unraid POST requests (#8
|
|||||||
// apiFetch adds the `api` prefix to the request URL,
|
// apiFetch adds the `api` prefix to the request URL,
|
||||||
// so endpoint should be provided without the `api` prefix
|
// so endpoint should be provided without the `api` prefix
|
||||||
// (i.e. provide `/data` rather than `api/data`).
|
// (i.e. provide `/data` rather than `api/data`).
|
||||||
export function apiFetch(
|
export function apiFetch<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: "GET" | "POST" | "PATCH",
|
init?: RequestInit | undefined
|
||||||
body?: any,
|
): Promise<T> {
|
||||||
params?: Record<string, string>
|
|
||||||
): Promise<Response> {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const nextParams = new URLSearchParams(params)
|
const nextParams = new URLSearchParams()
|
||||||
if (synoToken) {
|
if (synoToken) {
|
||||||
nextParams.set("SynoToken", synoToken)
|
nextParams.set("SynoToken", synoToken)
|
||||||
} else {
|
} else {
|
||||||
@ -31,36 +29,43 @@ export function apiFetch(
|
|||||||
const url = `api${endpoint}${search ? `?${search}` : ""}`
|
const url = `api${endpoint}${search ? `?${search}` : ""}`
|
||||||
|
|
||||||
var contentType: string
|
var contentType: string
|
||||||
if (unraidCsrfToken && method === "POST") {
|
if (unraidCsrfToken && init?.method === "POST") {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.append("csrf_token", unraidCsrfToken)
|
params.append("csrf_token", unraidCsrfToken)
|
||||||
if (body) {
|
if (init.body) {
|
||||||
params.append("ts_data", JSON.stringify(body))
|
params.append("ts_data", init.body.toString())
|
||||||
}
|
}
|
||||||
body = params.toString()
|
init.body = params.toString()
|
||||||
contentType = "application/x-www-form-urlencoded;charset=UTF-8"
|
contentType = "application/x-www-form-urlencoded;charset=UTF-8"
|
||||||
} else {
|
} else {
|
||||||
body = body ? JSON.stringify(body) : undefined
|
|
||||||
contentType = "application/json"
|
contentType = "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: method,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": contentType,
|
"Content-Type": contentType,
|
||||||
"X-CSRF-Token": csrfToken,
|
"X-CSRF-Token": csrfToken,
|
||||||
},
|
},
|
||||||
body,
|
|
||||||
}).then((r) => {
|
|
||||||
updateCsrfToken(r)
|
|
||||||
if (!r.ok) {
|
|
||||||
return r.text().then((err) => {
|
|
||||||
throw new Error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
})
|
})
|
||||||
|
.then((r) => {
|
||||||
|
updateCsrfToken(r)
|
||||||
|
if (!r.ok) {
|
||||||
|
return r.text().then((err) => {
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((r) => {
|
||||||
|
// TODO: MAYBE SET USING TOKEN HEADER
|
||||||
|
if (r.IsUnraid && r.UnraidToken) {
|
||||||
|
setUnraidCsrfToken(r.UnraidToken)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCsrfToken(r: Response) {
|
function updateCsrfToken(r: Response) {
|
||||||
@ -77,3 +82,36 @@ export function setSynoToken(token?: string) {
|
|||||||
export function setUnraidCsrfToken(token?: string) {
|
export function setUnraidCsrfToken(token?: string) {
|
||||||
unraidCsrfToken = token
|
unraidCsrfToken = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some fetch wrappers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getAuthSessionNew(): Promise<void> {
|
||||||
|
const d = await apiFetch<{ authUrl: string }>("/auth/session/new", {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
if (d.authUrl) {
|
||||||
|
window.open(d.authUrl, "_blank")
|
||||||
|
await apiFetch("/auth/session/wait", { method: "GET" })
|
||||||
|
}
|
||||||
|
// todo: still need catch for these, not using swr
|
||||||
|
}
|
||||||
|
|
||||||
|
type PatchLocalPrefsData = {
|
||||||
|
RunSSHSet?: boolean
|
||||||
|
RunSSH?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patchLocalPrefs(p: PatchLocalPrefsData): Promise<void> {
|
||||||
|
return apiFetch("/local/v0/prefs", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(p), // todo: annoying to do this for all...
|
||||||
|
})
|
||||||
|
// .then(onComplete)
|
||||||
|
// .catch((err) => {
|
||||||
|
// onComplete()
|
||||||
|
// alert("Failed to update prefs")
|
||||||
|
// throw err
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import React, { useEffect } from "react"
|
import React, { useEffect } from "react"
|
||||||
|
import { getAuthSessionNew } from "src/api"
|
||||||
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
|
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
|
||||||
import LoginToggle from "src/components/login-toggle"
|
import LoginToggle from "src/components/login-toggle"
|
||||||
import DeviceDetailsView from "src/components/views/device-details-view"
|
import DeviceDetailsView from "src/components/views/device-details-view"
|
||||||
@ -12,29 +13,24 @@ import SubnetRouterView from "src/components/views/subnet-router-view"
|
|||||||
import { UpdatingView } from "src/components/views/updating-view"
|
import { UpdatingView } from "src/components/views/updating-view"
|
||||||
import useAuth, { AuthResponse } from "src/hooks/auth"
|
import useAuth, { AuthResponse } from "src/hooks/auth"
|
||||||
import useNodeData, { NodeData } from "src/hooks/node-data"
|
import useNodeData, { NodeData } from "src/hooks/node-data"
|
||||||
|
import { useSWRConfig } from "swr"
|
||||||
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { data: auth, loading: loadingAuth, newSession } = useAuth()
|
const { data: auth, loading: loadingAuth } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-w-sm max-w-lg mx-auto py-14 px-5">
|
<main className="min-w-sm max-w-lg mx-auto py-14 px-5">
|
||||||
{loadingAuth || !auth ? (
|
{loadingAuth || !auth ? (
|
||||||
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
|
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
|
||||||
) : (
|
) : (
|
||||||
<WebClient auth={auth} newSession={newSession} />
|
<WebClient auth={auth} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WebClient({
|
function WebClient({ auth }: { auth: AuthResponse }) {
|
||||||
auth,
|
|
||||||
newSession,
|
|
||||||
}: {
|
|
||||||
auth: AuthResponse
|
|
||||||
newSession: () => Promise<void>
|
|
||||||
}) {
|
|
||||||
const { data, refreshData, nodeUpdaters } = useNodeData()
|
const { data, refreshData, nodeUpdaters } = useNodeData()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshData()
|
refreshData()
|
||||||
@ -51,7 +47,7 @@ function WebClient({
|
|||||||
// Otherwise render the new web client.
|
// Otherwise render the new web client.
|
||||||
<>
|
<>
|
||||||
<Router base={data.URLPrefix}>
|
<Router base={data.URLPrefix}>
|
||||||
<Header node={data} auth={auth} newSession={newSession} />
|
<Header node={data} auth={auth} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<HomeView
|
<HomeView
|
||||||
@ -93,15 +89,8 @@ function WebClient({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header({
|
function Header({ node, auth }: { node: NodeData; auth: AuthResponse }) {
|
||||||
node,
|
const { mutate } = useSWRConfig()
|
||||||
auth,
|
|
||||||
newSession,
|
|
||||||
}: {
|
|
||||||
node: NodeData
|
|
||||||
auth: AuthResponse
|
|
||||||
newSession: () => Promise<void>
|
|
||||||
}) {
|
|
||||||
const [loc] = useLocation()
|
const [loc] = useLocation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -113,7 +102,11 @@ function Header({
|
|||||||
{node.DomainName}
|
{node.DomainName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LoginToggle node={node} auth={auth} newSession={newSession} />
|
<LoginToggle
|
||||||
|
node={node}
|
||||||
|
auth={auth}
|
||||||
|
newSession={() => getAuthSessionNew().then(() => mutate("/data"))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{loc !== "/" && loc !== "/update" && (
|
{loc !== "/" && loc !== "/update" && (
|
||||||
<Link
|
<Link
|
||||||
|
@ -6,6 +6,7 @@ import * as Control from "src/components/control-components"
|
|||||||
import { NodeData, NodeUpdaters } from "src/hooks/node-data"
|
import { NodeData, NodeUpdaters } from "src/hooks/node-data"
|
||||||
import Toggle from "src/ui/toggle"
|
import Toggle from "src/ui/toggle"
|
||||||
|
|
||||||
|
// todo: htis fist
|
||||||
export default function SSHView({
|
export default function SSHView({
|
||||||
readonly,
|
readonly,
|
||||||
node,
|
node,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useEffect } from "react"
|
||||||
import { apiFetch, setSynoToken } from "src/api"
|
import { getAuthSessionNew, setSynoToken } from "src/api"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
synology = "synology",
|
synology = "synology",
|
||||||
@ -23,57 +24,29 @@ export type AuthResponse = {
|
|||||||
// useAuth reports and refreshes Tailscale auth status
|
// useAuth reports and refreshes Tailscale auth status
|
||||||
// for the web client.
|
// for the web client.
|
||||||
export default function useAuth() {
|
export default function useAuth() {
|
||||||
const [data, setData] = useState<AuthResponse>()
|
const { data, isLoading, mutate } = useSWR<AuthResponse>("/auth")
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
|
||||||
|
|
||||||
const loadAuth = useCallback(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
if (data?.authNeeded === AuthType.synology) {
|
||||||
return apiFetch("/auth", "GET")
|
fetch("/webman/login.cgi")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => {
|
.then((a) => {
|
||||||
setData(d)
|
setSynoToken(a.SynoToken)
|
||||||
switch ((d as AuthResponse).authNeeded) {
|
// Refresh auth reponse once synology
|
||||||
case AuthType.synology:
|
// auth completed.
|
||||||
fetch("/webman/login.cgi")
|
mutate()
|
||||||
.then((r) => r.json())
|
})
|
||||||
.then((a) => {
|
}
|
||||||
setSynoToken(a.SynoToken)
|
})
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setLoading(false)
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const newSession = useCallback(() => {
|
|
||||||
return apiFetch("/auth/session/new", "GET")
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
|
||||||
if (d.authUrl) {
|
|
||||||
window.open(d.authUrl, "_blank")
|
|
||||||
return apiFetch("/auth/session/wait", "GET")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => loadAuth())
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
}, [loadAuth])
|
|
||||||
|
|
||||||
|
// TODO
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAuth().then((d) => {
|
loadAuth().then((d) => {
|
||||||
if (
|
if (
|
||||||
!d.canManageNode &&
|
!d.canManageNode &&
|
||||||
new URLSearchParams(window.location.search).get("check") === "now"
|
new URLSearchParams(window.location.search).get("check") === "now"
|
||||||
) {
|
) {
|
||||||
newSession()
|
getAuthSessionNew()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -81,7 +54,6 @@ export default function useAuth() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
loading,
|
loading: isLoading || data?.authNeeded === AuthType.synology,
|
||||||
newSession,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useMemo } from "react"
|
||||||
import { apiFetch } from "src/api"
|
import { NodeData } from "src/hooks/node-data"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
export type ExitNode = {
|
export type ExitNode = {
|
||||||
ID: string
|
ID: string
|
||||||
@ -28,17 +29,19 @@ export type ExitNodeGroup = {
|
|||||||
nodes: ExitNode[]
|
nodes: ExitNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useExitNodes(tailnetName: string, filter?: string) {
|
export default function useExitNodes(filter?: string) {
|
||||||
const [data, setData] = useState<ExitNode[]>([])
|
const { data: node } = useSWR<NodeData>("/data")
|
||||||
|
const { data } = useSWR<ExitNode[]>("/exit-nodes") // TODO: PIPE BACK ERRORS, MAYBE SWR HAS SOMETHING GOOD
|
||||||
|
// const [data, setData] = useState<ExitNode[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
apiFetch("/exit-nodes", "GET")
|
// apiFetch("/exit-nodes", "GET")
|
||||||
.then((r) => r.json())
|
// .then((r) => r.json())
|
||||||
.then((r) => setData(r))
|
// .then((r) => setData(r))
|
||||||
.catch((err) => {
|
// .catch((err) => {
|
||||||
alert("Failed operation: " + err.message)
|
// alert("Failed operation: " + err.message)
|
||||||
})
|
// })
|
||||||
}, [])
|
// }, [])
|
||||||
|
|
||||||
const { tailnetNodesSorted, locationNodesMap } = useMemo(() => {
|
const { tailnetNodesSorted, locationNodesMap } = useMemo(() => {
|
||||||
// First going through exit nodes and splitting them into two groups:
|
// First going through exit nodes and splitting them into two groups:
|
||||||
@ -55,7 +58,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
// Only Mullvad exit nodes have locations filled.
|
// Only Mullvad exit nodes have locations filled.
|
||||||
tailnetNodes.push({
|
tailnetNodes.push({
|
||||||
...n,
|
...n,
|
||||||
Name: trimDNSSuffix(n.Name, tailnetName),
|
Name: trimDNSSuffix(n.Name, node?.TailnetName || ""),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -70,7 +73,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
|
|||||||
tailnetNodesSorted: tailnetNodes.sort(compareByName),
|
tailnetNodesSorted: tailnetNodes.sort(compareByName),
|
||||||
locationNodesMap: locationNodes,
|
locationNodesMap: locationNodes,
|
||||||
}
|
}
|
||||||
}, [data, tailnetName])
|
}, [data, node?.TailnetName])
|
||||||
|
|
||||||
const hasFilter = Boolean(filter)
|
const hasFilter = Boolean(filter)
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { apiFetch, setUnraidCsrfToken } from "src/api"
|
import { apiFetch } from "src/api"
|
||||||
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
||||||
import { VersionInfo } from "src/hooks/self-update"
|
import { VersionInfo } from "src/hooks/self-update"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
Profile: UserProfile
|
Profile: UserProfile
|
||||||
@ -93,21 +94,10 @@ type RoutesPOSTData = {
|
|||||||
|
|
||||||
// useNodeData returns basic data about the current node.
|
// useNodeData returns basic data about the current node.
|
||||||
export default function useNodeData() {
|
export default function useNodeData() {
|
||||||
const [data, setData] = useState<NodeData>()
|
const { data, mutate } = useSWR<NodeData>("/data") // TODO: USE GLOBAL MUTATE!!!
|
||||||
|
// const [data, setData] = useState<NodeData>()
|
||||||
const [isPosting, setIsPosting] = useState<boolean>(false)
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
||||||
|
|
||||||
const refreshData = useCallback(
|
|
||||||
() =>
|
|
||||||
apiFetch("/data", "GET")
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d: NodeData) => {
|
|
||||||
setData(d)
|
|
||||||
setUnraidCsrfToken(d.IsUnraid ? d.UnraidToken : undefined)
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error)),
|
|
||||||
[setData]
|
|
||||||
)
|
|
||||||
|
|
||||||
const prefsPATCH = useCallback(
|
const prefsPATCH = useCallback(
|
||||||
(d: PrefsPATCHData) => {
|
(d: PrefsPATCHData) => {
|
||||||
setIsPosting(true)
|
setIsPosting(true)
|
||||||
@ -120,12 +110,12 @@ export default function useNodeData() {
|
|||||||
// then make the prefs PATCH. If the request fails,
|
// then make the prefs PATCH. If the request fails,
|
||||||
// data will be updated to it's previous value in
|
// data will be updated to it's previous value in
|
||||||
// onComplete below.
|
// onComplete below.
|
||||||
setData(optimisticUpdates)
|
mutate(optimisticUpdates, { revalidate: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
setIsPosting(false)
|
setIsPosting(false)
|
||||||
refreshData() // refresh data after PATCH finishes
|
mutate() // refresh data after PATCH finishes
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiFetch("/local/v0/prefs", "PATCH", d)
|
return apiFetch("/local/v0/prefs", "PATCH", d)
|
||||||
@ -136,7 +126,7 @@ export default function useNodeData() {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setIsPosting, refreshData, setData, data]
|
[data, mutate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const routesPOST = useCallback(
|
const routesPOST = useCallback(
|
||||||
@ -144,7 +134,7 @@ export default function useNodeData() {
|
|||||||
setIsPosting(true)
|
setIsPosting(true)
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
setIsPosting(false)
|
setIsPosting(false)
|
||||||
refreshData() // refresh data after POST finishes
|
mutate() // refresh data after POST finishes
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiFetch("/routes", "POST", d)
|
return apiFetch("/routes", "POST", d)
|
||||||
@ -155,27 +145,27 @@ export default function useNodeData() {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setIsPosting, refreshData]
|
[setIsPosting, mutate]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(
|
// useEffect(
|
||||||
() => {
|
// () => {
|
||||||
// Initial data load.
|
// // Initial data load.
|
||||||
refreshData()
|
// refreshData()
|
||||||
|
|
||||||
// Refresh on browser tab focus.
|
// // Refresh on browser tab focus.
|
||||||
const onVisibilityChange = () => {
|
// const onVisibilityChange = () => {
|
||||||
document.visibilityState === "visible" && refreshData()
|
// document.visibilityState === "visible" && refreshData()
|
||||||
}
|
// }
|
||||||
window.addEventListener("visibilitychange", onVisibilityChange)
|
// window.addEventListener("visibilitychange", onVisibilityChange)
|
||||||
return () => {
|
// return () => {
|
||||||
// Cleanup browser tab listener.
|
// // Cleanup browser tab listener.
|
||||||
window.removeEventListener("visibilitychange", onVisibilityChange)
|
// window.removeEventListener("visibilitychange", onVisibilityChange)
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
// Run once.
|
// // Run once.
|
||||||
[refreshData]
|
// [refreshData]
|
||||||
)
|
// )
|
||||||
|
|
||||||
const nodeUpdaters: NodeUpdaters = useMemo(
|
const nodeUpdaters: NodeUpdaters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import App from "src/components/app"
|
import App from "src/components/app"
|
||||||
|
import { SWRConfig } from "swr"
|
||||||
|
import { apiFetch } from "./api"
|
||||||
|
|
||||||
declare var window: any
|
declare var window: any
|
||||||
// This is used to determine if the react client is built.
|
// This is used to determine if the react client is built.
|
||||||
@ -25,6 +27,19 @@ const root = createRoot(rootEl)
|
|||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fetcher: apiFetch,
|
||||||
|
onError: (err, _) => {
|
||||||
|
// TODO: toast on error instead?
|
||||||
|
if (err.message) {
|
||||||
|
alert(`Request failed: ${err.message}`)
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<App />
|
||||||
|
</SWRConfig>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
@ -2587,6 +2587,11 @@ classnames@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||||
|
|
||||||
|
client-only@^0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
@ -4701,6 +4706,14 @@ svg-parser@^2.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
|
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
|
||||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||||
|
|
||||||
|
swr@^2.2.4:
|
||||||
|
version "2.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
|
||||||
|
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
|
||||||
|
dependencies:
|
||||||
|
client-only "^0.0.1"
|
||||||
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
tailwindcss@^3.3.3:
|
tailwindcss@^3.3.3:
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
|
||||||
@ -4953,7 +4966,7 @@ use-sidecar@^1.1.2:
|
|||||||
detect-node-es "^1.1.0"
|
detect-node-es "^1.1.0"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.0.0:
|
use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user