mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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:
parent
5a9e935597
commit
6e30c9d1fe
@ -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
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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 device’s page
|
this device’s page
|
||||||
|
@ -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, learn more at{" "}
|
Or, learn 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 →
|
Learn more →
|
||||||
</a>
|
</a>
|
||||||
|
@ -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 →
|
Learn more →
|
||||||
</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>{" "}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
3685
client/web/yarn.lock
3685
client/web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user