fix(settings): DNS cache UI consistency, validation, and conditional rendering (#2382)

This commit is contained in:
fallenbagel
2026-02-13 04:16:10 +05:00
committed by GitHub
parent 3dea58eead
commit 91261f6a61
3 changed files with 178 additions and 125 deletions

View File

@@ -69,6 +69,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
dnsCacheGlobalStats: 'Global DNS Cache Stats', dnsCacheGlobalStats: 'Global DNS Cache Stats',
dnsCacheGlobalStatsDescription: dnsCacheGlobalStatsDescription:
'These stats are aggregated across all DNS cache entries.', 'These stats are aggregated across all DNS cache entries.',
dnsNoCacheEntries: 'No DNS lookups have been cached yet.',
size: 'Size', size: 'Size',
hits: 'Hits', hits: 'Hits',
misses: 'Misses', misses: 'Misses',
@@ -611,91 +612,133 @@ const SettingsJobs = () => {
</Table.TBody> </Table.TBody>
</Table> </Table>
</div> </div>
<div> {cacheData?.dnsCache != null && (
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3> <>
<p className="description"> <div>
{intl.formatMessage(messages.dnsCacheDescription)} <h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
</p> <p className="description">
</div> {intl.formatMessage(messages.dnsCacheDescription)}
<div className="section"> </p>
<Table> </div>
<thead> <div className="section">
<tr> <Table>
<Table.TH>{intl.formatMessage(messages.dnscachename)}</Table.TH> <thead>
<Table.TH> <tr>
{intl.formatMessage(messages.dnscacheactiveaddress)} <Table.TH>
</Table.TH> {intl.formatMessage(messages.dnscachename)}
<Table.TH>{intl.formatMessage(messages.dnscachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscacheage)}</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{Object.entries(cacheData?.dnsCache.entries || {}).map(
([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushdnscache)}</span>
</Button>
</Table.TD>
</tr>
)
)}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH> </Table.TH>
))} <Table.TH>
</tr> {intl.formatMessage(messages.dnscacheactiveaddress)}
</thead> </Table.TH>
<Table.TBody> <Table.TH>
<tr> {intl.formatMessage(messages.dnscachehits)}
{Object.entries(cacheData?.dnsCache.stats || {}) </Table.TH>
.filter(([statName]) => statName !== 'maxSize') <Table.TH>
.map(([statName, statValue]) => ( {intl.formatMessage(messages.dnscachemisses)}
<Table.TD key={`dns-stat-${statName}`}> </Table.TH>
{statName === 'hitRate' <Table.TH>
? intl.formatNumber(statValue, { {intl.formatMessage(messages.dnscacheage)}
style: 'percent', </Table.TH>
maximumFractionDigits: 2, <Table.TH></Table.TH>
}) </tr>
: intl.formatNumber(statValue)} </thead>
</Table.TD> <Table.TBody>
))} {(() => {
</tr> if (!cacheData) {
</Table.TBody> return (
</Table> <tr>
</div> <Table.TD colSpan={6} alignText="center">
<LoadingSpinner />
</Table.TD>
</tr>
);
}
const entries = Object.entries(
cacheData.dnsCache?.entries ?? {}
);
if (entries.length === 0) {
return (
<tr>
<Table.TD colSpan={6} alignText="center">
{intl.formatMessage(messages.dnsNoCacheEntries)}
</Table.TD>
</tr>
);
}
return entries.map(([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>
{intl.formatMessage(messages.flushdnscache)}
</span>
</Button>
</Table.TD>
</tr>
));
})()}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
{!cacheData ? (
<LoadingSpinner />
) : (
<Table>
<thead>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
)}
</div>
</>
)}
<div className="break-words"> <div className="break-words">
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3> <h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description"> <p className="description">

View File

@@ -29,6 +29,8 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
trustProxyTip: trustProxyTip:
'Allow Seerr to correctly register client IP addresses behind a proxy', 'Allow Seerr to correctly register client IP addresses behind a proxy',
proxyEnabled: 'HTTP(S) Proxy', proxyEnabled: 'HTTP(S) Proxy',
proxyEnabledTip:
'Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.',
proxyHostname: 'Proxy Hostname', proxyHostname: 'Proxy Hostname',
proxyPort: 'Proxy Port', proxyPort: 'Proxy Port',
proxySsl: 'Use SSL For Proxy', proxySsl: 'Use SSL For Proxy',
@@ -78,13 +80,16 @@ const SettingsNetwork = () => {
then: Yup.number() then: Yup.number()
.typeError(intl.formatMessage(messages.validationDnsCacheMaxTtl)) .typeError(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.required(intl.formatMessage(messages.validationDnsCacheMaxTtl)) .required(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.min(0), .min(-1),
}), }),
proxyPort: Yup.number().when('proxyEnabled', { proxyPort: Yup.number().when('proxyEnabled', {
is: (proxyEnabled: boolean) => proxyEnabled, is: (proxyEnabled: boolean) => proxyEnabled,
then: Yup.number().required( then: Yup.number()
intl.formatMessage(messages.validationProxyPort) .typeError(intl.formatMessage(messages.validationProxyPort))
), .integer(intl.formatMessage(messages.validationProxyPort))
.min(1, intl.formatMessage(messages.validationProxyPort))
.max(65535, intl.formatMessage(messages.validationProxyPort))
.required(intl.formatMessage(messages.validationProxyPort)),
}), }),
}); });
@@ -288,50 +293,50 @@ const SettingsNetwork = () => {
<div className="form-row"> <div className="form-row">
<label <label
htmlFor="dnsCacheForceMinTtl" htmlFor="dnsCacheForceMinTtl"
className="checkbox-label" className="text-label"
> >
{intl.formatMessage(messages.dnsCacheForceMinTtl)} {intl.formatMessage(messages.dnsCacheForceMinTtl)}
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
<div className="form-input-field"> <Field
<Field id="dnsCacheForceMinTtl"
id="dnsCacheForceMinTtl" name="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl" type="text"
type="number" inputMode="numeric"
/> className="short"
</div> />
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div> </div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div> </div>
<div className="form-row"> <div className="form-row">
<label <label
htmlFor="dnsCacheForceMaxTtl" htmlFor="dnsCacheForceMaxTtl"
className="checkbox-label" className="text-label"
> >
{intl.formatMessage(messages.dnsCacheForceMaxTtl)} {intl.formatMessage(messages.dnsCacheForceMaxTtl)}
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
<div className="form-input-field"> <Field
<Field id="dnsCacheForceMaxTtl"
id="dnsCacheForceMaxTtl" name="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl" type="text"
type="number" inputMode="text"
/> className="short"
</div> />
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div> </div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div> </div>
</div> </div>
</> </>
@@ -343,6 +348,9 @@ const SettingsNetwork = () => {
</span> </span>
<SettingsBadge badgeType="advanced" className="mr-2" /> <SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" /> <SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.proxyEnabledTip)}
</span>
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
<Field <Field
@@ -387,13 +395,13 @@ const SettingsNetwork = () => {
{intl.formatMessage(messages.proxyPort)} {intl.formatMessage(messages.proxyPort)}
</label> </label>
<div className="form-input-area"> <div className="form-input-area">
<div className="form-input-field"> <Field
<Field id="proxyPort"
id="proxyPort" name="proxyPort"
name="proxyPort" type="text"
type="number" inputMode="numeric"
/> className="short"
</div> />
{errors.proxyPort && {errors.proxyPort &&
touched.proxyPort && touched.proxyPort &&
typeof errors.proxyPort === 'string' && ( typeof errors.proxyPort === 'string' && (

View File

@@ -890,6 +890,7 @@
"components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.", "components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Cache Stats", "components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Cache Stats",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "These stats are aggregated across all DNS cache entries.", "components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "These stats are aggregated across all DNS cache entries.",
"components.Settings.SettingsJobsCache.dnsNoCacheEntries": "No DNS lookups have been cached yet.",
"components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Active Address", "components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Active Address",
"components.Settings.SettingsJobsCache.dnscacheage": "Age", "components.Settings.SettingsJobsCache.dnscacheage": "Age",
"components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns cache flushed.", "components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns cache flushed.",
@@ -1015,6 +1016,7 @@
"components.Settings.SettingsNetwork.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains", "components.Settings.SettingsNetwork.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains",
"components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses", "components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses",
"components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy", "components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy",
"components.Settings.SettingsNetwork.proxyEnabledTip": "Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.",
"components.Settings.SettingsNetwork.proxyHostname": "Proxy Hostname", "components.Settings.SettingsNetwork.proxyHostname": "Proxy Hostname",
"components.Settings.SettingsNetwork.proxyPassword": "Proxy Password", "components.Settings.SettingsNetwork.proxyPassword": "Proxy Password",
"components.Settings.SettingsNetwork.proxyPort": "Proxy Port", "components.Settings.SettingsNetwork.proxyPort": "Proxy Port",