mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
chore: setup react-you-might-not-need-an-effect
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ export default defineConfig({
|
||||
jsPlugins: [
|
||||
"@tanstack/eslint-plugin-query",
|
||||
"@tanstack/eslint-plugin-router",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect",
|
||||
],
|
||||
options: {
|
||||
typeAware: true,
|
||||
@@ -24,7 +25,6 @@ export default defineConfig({
|
||||
"@tanstack/query/stable-query-client": "error",
|
||||
"@tanstack/router/create-route-property-order": "error",
|
||||
"@tanstack/router/route-param-names": "error",
|
||||
"react/jsx-handler-names": "off",
|
||||
"eslint/complexity": "off",
|
||||
"eslint/func-style": [
|
||||
"error",
|
||||
@@ -42,6 +42,18 @@ export default defineConfig({
|
||||
"eslint/sort-keys": "off",
|
||||
"import/consistent-type-specifier-style": "off",
|
||||
"jsx-a11y/prefer-tag-over-role": "off",
|
||||
"react/jsx-handler-names": "off",
|
||||
"react-you-might-not-need-an-effect/no-adjust-state-on-prop-change":
|
||||
"error",
|
||||
"react-you-might-not-need-an-effect/no-chain-state-updates": "error",
|
||||
"react-you-might-not-need-an-effect/no-derived-state": "error",
|
||||
"react-you-might-not-need-an-effect/no-event-handler": "error",
|
||||
"react-you-might-not-need-an-effect/no-external-store-subscription":
|
||||
"error",
|
||||
"react-you-might-not-need-an-effect/no-initialize-state": "error",
|
||||
"react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
|
||||
"react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change":
|
||||
"error",
|
||||
"typescript/no-confusing-void-expression": "off",
|
||||
"typescript/no-floating-promises": [
|
||||
"error",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"happy-dom": "^20.10.6",
|
||||
"oxfmt": "^0.59.0",
|
||||
"oxlint": "^1.74.0",
|
||||
|
||||
@@ -135,6 +135,9 @@ importers:
|
||||
babel-plugin-react-compiler:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
eslint-plugin-react-you-might-not-need-an-effect:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(eslint@9.39.4(jiti@2.7.0))
|
||||
happy-dom:
|
||||
specifier: ^20.10.6
|
||||
version: 20.10.6
|
||||
@@ -1711,6 +1714,12 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-plugin-react-you-might-not-need-an-effect@1.0.1:
|
||||
resolution: {integrity: sha512-oOhQTYhor88Xp8RVytq25tvBfiAjU0r9SCDC51Qop+3Wg5BR1xGMAkM+/dV4MZbcMhdaU1L9bkv6LC95JmiTig==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
eslint: '>=8.40.0'
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1831,6 +1840,10 @@ packages:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@16.5.0:
|
||||
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
@@ -4061,6 +4074,11 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-plugin-react-you-might-not-need-an-effect@1.0.1(eslint@9.39.4(jiti@2.7.0)):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.7.0)
|
||||
globals: 16.5.0
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
@@ -4192,6 +4210,8 @@ snapshots:
|
||||
|
||||
globals@14.0.0: {}
|
||||
|
||||
globals@16.5.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
happy-dom@20.10.6:
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { Pencil, Trash2 } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form"
|
||||
|
||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||||
@@ -196,14 +196,17 @@ export function ListenerRulesTab({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<RuleDialog
|
||||
isPending={createMutation.isPending}
|
||||
mode="add"
|
||||
onOpenChange={setAddOpen}
|
||||
onSubmit={handleCreate}
|
||||
open={addOpen}
|
||||
targetGroups={targetGroups}
|
||||
/>
|
||||
{/* Mounted only while open so every session starts from blank defaults. */}
|
||||
{addOpen && (
|
||||
<RuleDialog
|
||||
isPending={createMutation.isPending}
|
||||
mode="add"
|
||||
onOpenChange={setAddOpen}
|
||||
onSubmit={handleCreate}
|
||||
open={addOpen}
|
||||
targetGroups={targetGroups}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RuleDialog
|
||||
initialRule={editTarget}
|
||||
@@ -320,6 +323,25 @@ function toRuleCondInput(c: RuleConditionFormData): RuleCondInput {
|
||||
}
|
||||
}
|
||||
|
||||
// A new rule starts at the highest priority with a single empty path match.
|
||||
const DEFAULT_PRIORITY = 1
|
||||
const defaultConditions: RuleConditionFormData[] = [
|
||||
{ field: "path-pattern", rawValues: "" },
|
||||
]
|
||||
|
||||
// Projects an existing rule onto the form shape, falling back to the defaults
|
||||
// for a non-numeric priority or a rule that carries no conditions.
|
||||
function ruleFormValues(rule: Rule): CreateRuleFormData {
|
||||
const priority = Number(rule.Priority ?? DEFAULT_PRIORITY)
|
||||
const conditions: RuleConditionFormData[] =
|
||||
rule.Conditions?.map((c) => fromSDKCondition(c)) ?? []
|
||||
return {
|
||||
priority: Number.isFinite(priority) ? priority : DEFAULT_PRIORITY,
|
||||
conditions: conditions.length > 0 ? conditions : defaultConditions,
|
||||
forwardTargetGroupArn: rule.Actions?.[0]?.TargetGroupArn ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
interface RuleDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -339,48 +361,22 @@ function RuleDialog({
|
||||
mode,
|
||||
initialRule,
|
||||
}: RuleDialogProps) {
|
||||
const defaultConds: RuleConditionFormData[] = [
|
||||
{ field: "path-pattern", rawValues: "" },
|
||||
]
|
||||
const form = useForm<CreateRuleFormData>({
|
||||
resolver: zodResolver(createRuleSchema),
|
||||
defaultValues: {
|
||||
priority: 1,
|
||||
conditions: defaultConds,
|
||||
priority: DEFAULT_PRIORITY,
|
||||
conditions: defaultConditions,
|
||||
forwardTargetGroupArn: "",
|
||||
},
|
||||
// Re-seeds the form whenever a different rule is selected. Left undefined
|
||||
// while closing so the fields do not visibly reset mid-animation.
|
||||
values: initialRule ? ruleFormValues(initialRule) : undefined,
|
||||
})
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "conditions",
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "add") {
|
||||
if (!open) {
|
||||
form.reset({
|
||||
priority: 1,
|
||||
conditions: defaultConds,
|
||||
forwardTargetGroupArn: "",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!initialRule) {
|
||||
return
|
||||
}
|
||||
const priorityNum = Number(initialRule.Priority ?? 1)
|
||||
const conditions: RuleConditionFormData[] =
|
||||
initialRule.Conditions?.map((c) => fromSDKCondition(c)) ?? defaultConds
|
||||
form.reset({
|
||||
priority: Number.isFinite(priorityNum) ? priorityNum : 1,
|
||||
conditions: conditions.length > 0 ? conditions : defaultConds,
|
||||
forwardTargetGroupArn: initialRule.Actions?.[0]?.TargetGroupArn ?? "",
|
||||
})
|
||||
// form is intentionally excluded so the reset only fires on identity change of the source.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialRule, open, mode])
|
||||
|
||||
const handleConfirm = form.handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Listener } from "@aws-sdk/client-elastic-load-balancing-v2"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSuspenseQuery } from "@tanstack/react-query"
|
||||
import { Pencil, Trash2 } from "lucide-react"
|
||||
import { Fragment, useEffect, useState } from "react"
|
||||
import { Fragment, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||||
@@ -56,6 +56,26 @@ function listenerDefaults(lbType: LbType): CreateListenerFormData {
|
||||
}
|
||||
}
|
||||
|
||||
// Projects an existing listener onto the form shape. TLS fields are dropped for
|
||||
// plaintext protocols so a downgrade does not carry a stale certificate.
|
||||
function listenerFormValues(
|
||||
listener: Listener,
|
||||
protocols: readonly ListenerProtocolValue[],
|
||||
): CreateListenerFormData {
|
||||
const protocol: ListenerProtocolValue =
|
||||
protocols.find((p) => p === listener.Protocol) ?? protocols[0] ?? "HTTP"
|
||||
const secure = isSecureProtocol(protocol)
|
||||
return {
|
||||
protocol,
|
||||
port: listener.Port ?? 80,
|
||||
defaultTargetGroupArn: listener.DefaultActions?.[0]?.TargetGroupArn ?? "",
|
||||
certificateArn: secure
|
||||
? listener.Certificates?.[0]?.CertificateArn
|
||||
: undefined,
|
||||
sslPolicy: protocol === "HTTPS" ? listener.SslPolicy : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function formatTls(listener: Listener): string {
|
||||
if (listener.Protocol !== "HTTPS") {
|
||||
return "—"
|
||||
@@ -276,17 +296,20 @@ export function ListenersTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AddListenerDialog
|
||||
certificates={certificates}
|
||||
isPending={createMutation.isPending}
|
||||
lbType={lbType}
|
||||
onOpenChange={setAddOpen}
|
||||
onSubmit={handleCreate}
|
||||
open={addOpen}
|
||||
protocols={protocols}
|
||||
sslPolicies={sslPolicies}
|
||||
targetGroups={vpcTgs}
|
||||
/>
|
||||
{/* Mounted only while open so every session starts from blank defaults. */}
|
||||
{addOpen && (
|
||||
<AddListenerDialog
|
||||
certificates={certificates}
|
||||
isPending={createMutation.isPending}
|
||||
lbType={lbType}
|
||||
onOpenChange={setAddOpen}
|
||||
onSubmit={handleCreate}
|
||||
open={addOpen}
|
||||
protocols={protocols}
|
||||
sslPolicies={sslPolicies}
|
||||
targetGroups={vpcTgs}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditListenerDialog
|
||||
certificates={certificates}
|
||||
@@ -343,12 +366,6 @@ function AddListenerDialog({
|
||||
defaultValues: listenerDefaults(lbType),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset(listenerDefaults(lbType))
|
||||
}
|
||||
}, [open, form, lbType])
|
||||
|
||||
const handleConfirm = form.handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
@@ -421,26 +438,11 @@ function EditListenerDialog({
|
||||
port: 80,
|
||||
defaultTargetGroupArn: "",
|
||||
},
|
||||
// Re-seeds the form whenever a different listener is selected. Left
|
||||
// undefined while closing so the fields do not visibly reset mid-animation.
|
||||
values: listener ? listenerFormValues(listener, protocols) : undefined,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!listener) {
|
||||
return
|
||||
}
|
||||
const protocol: ListenerProtocolValue =
|
||||
protocols.find((p) => p === listener.Protocol) ?? protocols[0] ?? "HTTP"
|
||||
const secure = isSecureProtocol(protocol)
|
||||
form.reset({
|
||||
protocol,
|
||||
port: listener.Port ?? 80,
|
||||
defaultTargetGroupArn: listener.DefaultActions?.[0]?.TargetGroupArn ?? "",
|
||||
certificateArn: secure
|
||||
? listener.Certificates?.[0]?.CertificateArn
|
||||
: undefined,
|
||||
sslPolicy: protocol === "HTTPS" ? listener.SslPolicy : undefined,
|
||||
})
|
||||
}, [listener, form, protocols])
|
||||
|
||||
const handleConfirm = form.handleSubmit(onSubmit)
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,6 +38,9 @@ export function ThemeProvider({
|
||||
getStoredTheme(storageKey, defaultTheme),
|
||||
)
|
||||
|
||||
// Syncing the theme class onto <html> is a genuine external-system write: it
|
||||
// has to run on mount for the stored theme, not just on user selection.
|
||||
/* oxlint-disable react-you-might-not-need-an-effect/no-event-handler */
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
@@ -55,6 +58,7 @@ export function ThemeProvider({
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
/* oxlint-enable react-you-might-not-need-an-effect/no-event-handler */
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
Reference in New Issue
Block a user