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,
"dependencies": {
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"classnames": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"wouter": "^2.11.0"
},
"devDependencies": {
"@types/classnames": "^2.2.10",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"eslint": "^8.23.1",
"eslint-config-react-app": "^7.0.1",
"postcss": "^8.4.31",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2",
@ -35,11 +36,26 @@
"scripts": {
"build": "vite build",
"start": "vite",
"lint": "tsc --noEmit",
"lint": "tsc --noEmit && eslint 'src/**/*.{ts,tsx,js,jsx}'",
"test": "vitest",
"format": "prettier --write '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": {
"semi": false,
"printWidth": 80

View File

@ -1,7 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useEffect } from "react"
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
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 [
@ -261,7 +261,7 @@ function ExitNodeSelectorInner({
key={`${n.ID}-${n.Name}`}
node={n}
onSelect={() => onSelect(n)}
isSelected={selected.ID == n.ID}
isSelected={selected.ID === n.ID}
/>
))}
</div>
@ -309,7 +309,7 @@ function ExitNodeSelectorItem({
function CountryFlag({ code }: { code: string }) {
return (
countryFlags[code.toLowerCase()] || (
<>{countryFlags[code.toLowerCase()]}</> || (
<span className="font-medium text-gray-500 text-xs">
{code.toUpperCase()}
</span>

View File

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

View File

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

View File

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

View File

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

View File

@ -65,17 +65,18 @@ export default function useAuth() {
.catch((error) => {
console.error(error)
})
}, [])
}, [loadAuth])
useEffect(() => {
loadAuth().then((d) => {
if (
!d.canManageNode &&
new URLSearchParams(window.location.search).get("check") == "now"
new URLSearchParams(window.location.search).get("check") === "now"
) {
newSession()
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {

View File

@ -71,6 +71,8 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
}
}, [data, tailnetName])
const hasFilter = Boolean(filter)
const mullvadNodesSorted = useMemo(() => {
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
// exit node per-country.
//
@ -121,7 +123,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
}
return nodes.sort(compareByName)
}, [locationNodesMap, Boolean(filter)])
}, [hasFilter, locationNodesMap])
// Ordered and filtered grouping of exit nodes.
const exitNodeGroups = useMemo(() => {
@ -165,7 +167,7 @@ function highestPriorityNode(nodes: ExitNode[]): ExitNode | undefined {
// compareName compares two exit nodes alphabetically by name.
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.
if (a.Name.includes(": Best Match")) {
return -1

View File

@ -120,7 +120,7 @@ export default function useNodeData() {
throw err
})
},
[data]
[data, isPosting, refreshData]
)
const updatePrefs = useCallback(
@ -169,7 +169,7 @@ export default function useNodeData() {
}
},
// Run once.
[]
[refreshData]
)
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,
// and returns state messages showing the progress of the update.
export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
if (!cv) {
return {
updateState: UpdateState.UpToDate,
updateLog: "",
}
}
const [updateState, setUpdateState] = useState<UpdateState>(
cv.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
cv?.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
)
const [updateLog, setUpdateLog] = useState<string>("")
@ -132,7 +125,10 @@ export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
if (timer) clearTimeout(timer)
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