client/web: add eslint

Add eslint to require stricter typescript rules, particularly around
required hook dependencies. This commit also updates any files that
were now throwing errors with eslint.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-11-28 16:31:56 -05:00 committed by Sonia Appasamy
parent 5a9e935597
commit 6e30c9d1fe
12 changed files with 3255 additions and 533 deletions

View File

@ -8,19 +8,20 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"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",
"wouter": "^2.11.0" "wouter": "^2.11.0"
}, },
"devDependencies": { "devDependencies": {
"@types/classnames": "^2.2.10",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint": "^8.23.1",
"eslint-config-react-app": "^7.0.1",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2", "prettier-plugin-organize-imports": "^3.2.2",
@ -35,11 +36,26 @@
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"start": "vite", "start": "vite",
"lint": "tsc --noEmit", "lint": "tsc --noEmit && eslint 'src/**/*.{ts,tsx,js,jsx}'",
"test": "vitest", "test": "vitest",
"format": "prettier --write 'src/**/*.{ts,tsx}'", "format": "prettier --write 'src/**/*.{ts,tsx}'",
"format-check": "prettier --check 'src/**/*.{ts,tsx}'" "format-check": "prettier --check 'src/**/*.{ts,tsx}'"
}, },
"eslintConfig": {
"extends": [
"react-app"
],
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error"
},
"settings": {
"projectRoot": "client/web/package.json"
}
},
"prettier": { "prettier": {
"semi": false, "semi": false,
"printWidth": 80 "printWidth": 80

View File

@ -1,7 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useEffect } from "react" import React, { useEffect } from "react"
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"
@ -121,22 +120,3 @@ function Header({
</> </>
) )
} }
function Footer({
licensesURL,
className,
}: {
licensesURL: string
className?: string
}) {
return (
<footer className={cx("container max-w-lg mx-auto text-center", className)}>
<a
className="text-xs text-gray-500 hover:text-gray-600"
href={licensesURL}
>
Open Source Licenses
</a>
</footer>
)
}

View File

@ -78,7 +78,7 @@ export default function ExitNodeSelector({
} }
} }
}, },
[setOpen, selected, setSelected] [selected, updateNode, updatePrefs]
) )
const [ const [
@ -261,7 +261,7 @@ function ExitNodeSelectorInner({
key={`${n.ID}-${n.Name}`} key={`${n.ID}-${n.Name}`}
node={n} node={n}
onSelect={() => onSelect(n)} onSelect={() => onSelect(n)}
isSelected={selected.ID == n.ID} isSelected={selected.ID === n.ID}
/> />
))} ))}
</div> </div>
@ -309,7 +309,7 @@ function ExitNodeSelectorItem({
function CountryFlag({ code }: { code: string }) { function CountryFlag({ code }: { code: string }) {
return ( return (
countryFlags[code.toLowerCase()] || ( <>{countryFlags[code.toLowerCase()]}</> || (
<span className="font-medium text-gray-500 text-xs"> <span className="font-medium text-gray-500 text-xs">
{code.toUpperCase()} {code.toUpperCase()}
</span> </span>

View File

@ -110,12 +110,7 @@ function LoginPopoverContent({
setCanConnectOverTS(true) setCanConnectOverTS(true)
}) })
.catch(() => setIsRunningCheck(false)) .catch(() => setIsRunningCheck(false))
}, [ }, [auth.viewerIdentity, isRunningCheck, node.IP])
auth.viewerIdentity,
isRunningCheck,
setCanConnectOverTS,
setIsRunningCheck,
])
/** /**
* Checking connection for first time on page load. * Checking connection for first time on page load.
@ -125,6 +120,7 @@ function LoginPopoverContent({
* leaving to turn on Tailscale then returning to the view. * leaving to turn on Tailscale then returning to the view.
* See `onMouseEnter` on the div below. * See `onMouseEnter` on the div below.
*/ */
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => checkTSConnection(), []) useEffect(() => checkTSConnection(), [])
const handleSignInClick = useCallback(() => { const handleSignInClick = useCallback(() => {
@ -145,7 +141,7 @@ function LoginPopoverContent({
{auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`} {auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
</div> </div>
{!auth.canManageNode && {!auth.canManageNode &&
(!auth.viewerIdentity || auth.authNeeded == AuthType.tailscale ? ( (!auth.viewerIdentity || auth.authNeeded === AuthType.tailscale ? (
<> <>
<p className="text-neutral-500 text-xs"> <p className="text-neutral-500 text-xs">
{auth.viewerIdentity ? ( {auth.viewerIdentity ? (

View File

@ -125,6 +125,7 @@ export default function DeviceDetailsView({
// TODO: pipe control serve url from backend // TODO: pipe control serve url from backend
href="https://login.tailscale.com/admin" href="https://login.tailscale.com/admin"
target="_blank" target="_blank"
rel="noreferrer"
className="text-indigo-700 text-sm" className="text-indigo-700 text-sm"
> >
this devices page this devices page

View File

@ -32,7 +32,7 @@ export default function LoginView({
return ( return (
<div className="mb-8 py-6 px-8 bg-white rounded-md shadow-2xl"> <div className="mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<TailscaleIcon className="my-2 mb-8" /> <TailscaleIcon className="my-2 mb-8" />
{data.Status == "Stopped" ? ( {data.Status === "Stopped" ? (
<> <>
<div className="mb-6"> <div className="mb-6">
<h3 className="text-3xl font-semibold mb-3">Connect</h3> <h3 className="text-3xl font-semibold mb-3">Connect</h3>
@ -57,6 +57,7 @@ export default function LoginView({
href="https://tailscale.com/kb/1028/key-expiry" href="https://tailscale.com/kb/1028/key-expiry"
className="link" className="link"
target="_blank" target="_blank"
rel="noreferrer"
> >
learn more learn more
</a> </a>
@ -77,7 +78,12 @@ export default function LoginView({
<p className="text-gray-700"> <p className="text-gray-700">
Get started by logging in to your Tailscale network. Get started by logging in to your Tailscale network.
Or,&nbsp;learn&nbsp;more at{" "} Or,&nbsp;learn&nbsp;more at{" "}
<a href="https://tailscale.com/" className="link" target="_blank"> <a
href="https://tailscale.com/"
className="link"
target="_blank"
rel="noreferrer"
>
tailscale.com tailscale.com
</a> </a>
. .
@ -103,6 +109,7 @@ export default function LoginView({
href="https://tailscale.com/kb/1085/auth-keys/" href="https://tailscale.com/kb/1085/auth-keys/"
className="link" className="link"
target="_blank" target="_blank"
rel="noreferrer"
> >
Learn more &rarr; Learn more &rarr;
</a> </a>

View File

@ -24,6 +24,7 @@ export default function SSHView({
href="https://tailscale.com/kb/1193/tailscale-ssh/" href="https://tailscale.com/kb/1193/tailscale-ssh/"
className="text-indigo-700" className="text-indigo-700"
target="_blank" target="_blank"
rel="noreferrer"
> >
Learn more &rarr; Learn more &rarr;
</a> </a>
@ -44,6 +45,7 @@ export default function SSHView({
href="https://login.tailscale.com/admin/acls/" href="https://login.tailscale.com/admin/acls/"
className="text-indigo-700" className="text-indigo-700"
target="_blank" target="_blank"
rel="noreferrer"
> >
tailnet policy file tailnet policy file
</a>{" "} </a>{" "}

View File

@ -65,17 +65,18 @@ export default function useAuth() {
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
}) })
}, []) }, [loadAuth])
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() newSession()
} }
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return { return {

View File

@ -71,6 +71,8 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
} }
}, [data, tailnetName]) }, [data, tailnetName])
const hasFilter = Boolean(filter)
const mullvadNodesSorted = useMemo(() => { const mullvadNodesSorted = useMemo(() => {
const nodes: ExitNode[] = [] const nodes: ExitNode[] = []
@ -91,7 +93,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
}) })
} }
if (!Boolean(filter)) { if (!hasFilter) {
// When nothing is searched, only show a single best-matching // When nothing is searched, only show a single best-matching
// exit node per-country. // exit node per-country.
// //
@ -121,7 +123,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
} }
return nodes.sort(compareByName) return nodes.sort(compareByName)
}, [locationNodesMap, Boolean(filter)]) }, [hasFilter, locationNodesMap])
// Ordered and filtered grouping of exit nodes. // Ordered and filtered grouping of exit nodes.
const exitNodeGroups = useMemo(() => { const exitNodeGroups = useMemo(() => {
@ -165,7 +167,7 @@ function highestPriorityNode(nodes: ExitNode[]): ExitNode | undefined {
// compareName compares two exit nodes alphabetically by name. // compareName compares two exit nodes alphabetically by name.
function compareByName(a: ExitNode, b: ExitNode): number { function compareByName(a: ExitNode, b: ExitNode): number {
if (a.Location && b.Location && a.Location.Country == b.Location.Country) { if (a.Location && b.Location && a.Location.Country === b.Location.Country) {
// Always put "<Country>: Best Match" node at top of country list. // Always put "<Country>: Best Match" node at top of country list.
if (a.Name.includes(": Best Match")) { if (a.Name.includes(": Best Match")) {
return -1 return -1

View File

@ -120,7 +120,7 @@ export default function useNodeData() {
throw err throw err
}) })
}, },
[data] [data, isPosting, refreshData]
) )
const updatePrefs = useCallback( const updatePrefs = useCallback(
@ -169,7 +169,7 @@ export default function useNodeData() {
} }
}, },
// Run once. // Run once.
[] [refreshData]
) )
return { data, refreshData, updateNode, updatePrefs, isPosting } return { data, refreshData, updateNode, updatePrefs, isPosting }

View File

@ -29,15 +29,8 @@ export enum UpdateState {
// useInstallUpdate initiates and tracks a Tailscale self-update via the LocalAPI, // useInstallUpdate initiates and tracks a Tailscale self-update via the LocalAPI,
// and returns state messages showing the progress of the update. // and returns state messages showing the progress of the update.
export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) { export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
if (!cv) {
return {
updateState: UpdateState.UpToDate,
updateLog: "",
}
}
const [updateState, setUpdateState] = useState<UpdateState>( const [updateState, setUpdateState] = useState<UpdateState>(
cv.RunningLatest ? UpdateState.UpToDate : UpdateState.Available cv?.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
) )
const [updateLog, setUpdateLog] = useState<string>("") const [updateLog, setUpdateLog] = useState<string>("")
@ -132,7 +125,10 @@ export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
if (timer) clearTimeout(timer) if (timer) clearTimeout(timer)
timer = 0 timer = 0
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return { updateState, updateLog } return !cv
? { updateState: UpdateState.UpToDate, updateLog: "" }
: { updateState, updateLog }
} }

File diff suppressed because it is too large Load Diff