fix(settings): DNS cache UI consistency, validation, and conditional rendering (#2382)
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user