mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
fix(config): validate visual numeric fields
This commit is contained in:
@@ -15,6 +15,8 @@ import type {
|
||||
PayloadParamEntry,
|
||||
PayloadParamValueType,
|
||||
PayloadRule,
|
||||
VisualConfigValidationErrorCode,
|
||||
VisualConfigValidationErrors,
|
||||
VisualConfigValues,
|
||||
} from '@/types/visualConfig';
|
||||
import { makeClientId } from '@/types/visualConfig';
|
||||
@@ -27,10 +29,19 @@ import { isValidApiKeyCharset } from '@/utils/validation';
|
||||
|
||||
interface VisualConfigEditorProps {
|
||||
values: VisualConfigValues;
|
||||
validationErrors?: VisualConfigValidationErrors;
|
||||
disabled?: boolean;
|
||||
onChange: (values: Partial<VisualConfigValues>) => void;
|
||||
}
|
||||
|
||||
function getValidationMessage(
|
||||
t: ReturnType<typeof useTranslation>['t'],
|
||||
errorCode?: VisualConfigValidationErrorCode
|
||||
) {
|
||||
if (!errorCode) return undefined;
|
||||
return t(`config_management.visual.validation.${errorCode}`);
|
||||
}
|
||||
|
||||
type ToggleRowProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
@@ -717,11 +728,21 @@ function PayloadFilterRulesEditor({
|
||||
);
|
||||
}
|
||||
|
||||
export function VisualConfigEditor({ values, disabled = false, onChange }: VisualConfigEditorProps) {
|
||||
export function VisualConfigEditor({ values, validationErrors, disabled = false, onChange }: VisualConfigEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const isKeepaliveDisabled = values.streaming.keepaliveSeconds === '' || values.streaming.keepaliveSeconds === '0';
|
||||
const isNonstreamKeepaliveDisabled =
|
||||
values.streaming.nonstreamKeepaliveInterval === '' || values.streaming.nonstreamKeepaliveInterval === '0';
|
||||
const portError = getValidationMessage(t, validationErrors?.port);
|
||||
const logsMaxSizeError = getValidationMessage(t, validationErrors?.logsMaxTotalSizeMb);
|
||||
const requestRetryError = getValidationMessage(t, validationErrors?.requestRetry);
|
||||
const maxRetryIntervalError = getValidationMessage(t, validationErrors?.maxRetryInterval);
|
||||
const keepaliveError = getValidationMessage(t, validationErrors?.['streaming.keepaliveSeconds']);
|
||||
const bootstrapRetriesError = getValidationMessage(t, validationErrors?.['streaming.bootstrapRetries']);
|
||||
const nonstreamKeepaliveError = getValidationMessage(
|
||||
t,
|
||||
validationErrors?.['streaming.nonstreamKeepaliveInterval']
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
@@ -741,6 +762,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
value={values.port}
|
||||
onChange={(e) => onChange({ port: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={portError}
|
||||
/>
|
||||
</SectionGrid>
|
||||
</ConfigSection>
|
||||
@@ -873,6 +895,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
value={values.logsMaxTotalSizeMb}
|
||||
onChange={(e) => onChange({ logsMaxTotalSizeMb: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={logsMaxSizeError}
|
||||
/>
|
||||
</SectionGrid>
|
||||
</div>
|
||||
@@ -895,6 +918,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
value={values.requestRetry}
|
||||
onChange={(e) => onChange({ requestRetry: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={requestRetryError}
|
||||
/>
|
||||
<Input
|
||||
label={t('config_management.visual.sections.network.max_retry_interval')}
|
||||
@@ -903,6 +927,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
value={values.maxRetryInterval}
|
||||
onChange={(e) => onChange({ maxRetryInterval: e.target.value })}
|
||||
disabled={disabled}
|
||||
error={maxRetryIntervalError}
|
||||
/>
|
||||
<div className="form-group">
|
||||
<label>{t('config_management.visual.sections.network.routing_strategy')}</label>
|
||||
@@ -993,6 +1018,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{keepaliveError && <div className="error-box">{keepaliveError}</div>}
|
||||
<div className="hint">{t('config_management.visual.sections.streaming.keepalive_hint')}</div>
|
||||
</div>
|
||||
<Input
|
||||
@@ -1003,6 +1029,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
onChange={(e) => onChange({ streaming: { ...values.streaming, bootstrapRetries: e.target.value } })}
|
||||
disabled={disabled}
|
||||
hint={t('config_management.visual.sections.streaming.bootstrap_hint')}
|
||||
error={bootstrapRetriesError}
|
||||
/>
|
||||
</SectionGrid>
|
||||
|
||||
@@ -1041,6 +1068,7 @@ export function VisualConfigEditor({ values, disabled = false, onChange }: Visua
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{nonstreamKeepaliveError && <div className="error-box">{nonstreamKeepaliveError}</div>}
|
||||
<div className="hint">
|
||||
{t('config_management.visual.sections.streaming.nonstream_keepalive_hint')}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
PayloadParamValueType,
|
||||
PayloadRule,
|
||||
VisualConfigValues,
|
||||
VisualConfigValidationErrors,
|
||||
} from '@/types/visualConfig';
|
||||
import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig';
|
||||
|
||||
@@ -93,13 +94,46 @@ function setIntFromStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
if (!/^-?\d+$/.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number(trimmed);
|
||||
if (Number.isFinite(parsed)) {
|
||||
doc.setIn(path, parsed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (docHas(doc, path)) doc.deleteIn(path);
|
||||
function getNonNegativeIntegerError(value: string): 'non_negative_integer' | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!/^-?\d+$/.test(trimmed)) return 'non_negative_integer';
|
||||
return Number(trimmed) >= 0 ? undefined : 'non_negative_integer';
|
||||
}
|
||||
|
||||
function getPortError(value: string): 'port_range' | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!/^\d+$/.test(trimmed)) return 'port_range';
|
||||
const parsed = Number(trimmed);
|
||||
return parsed >= 1 && parsed <= 65535 ? undefined : 'port_range';
|
||||
}
|
||||
|
||||
export function getVisualConfigValidationErrors(
|
||||
values: VisualConfigValues
|
||||
): VisualConfigValidationErrors {
|
||||
return {
|
||||
port: getPortError(values.port),
|
||||
logsMaxTotalSizeMb: getNonNegativeIntegerError(values.logsMaxTotalSizeMb),
|
||||
requestRetry: getNonNegativeIntegerError(values.requestRetry),
|
||||
maxRetryInterval: getNonNegativeIntegerError(values.maxRetryInterval),
|
||||
'streaming.keepaliveSeconds': getNonNegativeIntegerError(values.streaming.keepaliveSeconds),
|
||||
'streaming.bootstrapRetries': getNonNegativeIntegerError(values.streaming.bootstrapRetries),
|
||||
'streaming.nonstreamKeepaliveInterval': getNonNegativeIntegerError(
|
||||
values.streaming.nonstreamKeepaliveInterval
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function deepClone<T>(value: T): T {
|
||||
@@ -277,6 +311,10 @@ export function useVisualConfig() {
|
||||
...DEFAULT_VISUAL_VALUES,
|
||||
});
|
||||
const [visualParseError, setVisualParseError] = useState<string | null>(null);
|
||||
const visualValidationErrors = useMemo(
|
||||
() => getVisualConfigValidationErrors(visualValues),
|
||||
[visualValues]
|
||||
);
|
||||
|
||||
const visualDirty = useMemo(() => {
|
||||
return JSON.stringify(visualValues) !== JSON.stringify(baselineValues);
|
||||
@@ -536,6 +574,7 @@ export function useVisualConfig() {
|
||||
visualValues,
|
||||
visualDirty,
|
||||
visualParseError,
|
||||
visualValidationErrors,
|
||||
loadVisualValuesFromYaml,
|
||||
applyVisualChangesToYaml,
|
||||
setVisualValues,
|
||||
|
||||
@@ -1238,6 +1238,11 @@
|
||||
"value_json": "JSON value",
|
||||
"value_default": "Value"
|
||||
},
|
||||
"validation": {
|
||||
"validation_blocked": "Fix validation errors before saving",
|
||||
"port_range": "Enter a valid port between 1 and 65535",
|
||||
"non_negative_integer": "Enter a non-negative whole number"
|
||||
},
|
||||
"common": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
|
||||
@@ -1243,6 +1243,11 @@
|
||||
"value_json": "Значение JSON",
|
||||
"value_default": "Значение"
|
||||
},
|
||||
"validation": {
|
||||
"validation_blocked": "Исправьте ошибки валидации перед сохранением",
|
||||
"port_range": "Введите корректный порт от 1 до 65535",
|
||||
"non_negative_integer": "Введите неотрицательное целое число"
|
||||
},
|
||||
"common": {
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
|
||||
@@ -1238,6 +1238,11 @@
|
||||
"value_json": "JSON 值",
|
||||
"value_default": "值"
|
||||
},
|
||||
"validation": {
|
||||
"validation_blocked": "请先修复表单校验错误再保存",
|
||||
"port_range": "请输入 1 到 65535 之间的有效端口",
|
||||
"non_negative_integer": "请输入非负整数"
|
||||
},
|
||||
"common": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
|
||||
@@ -40,6 +40,7 @@ export function ConfigPage() {
|
||||
visualValues,
|
||||
visualDirty,
|
||||
visualParseError,
|
||||
visualValidationErrors,
|
||||
loadVisualValuesFromYaml,
|
||||
applyVisualChangesToYaml,
|
||||
setVisualValues
|
||||
@@ -72,6 +73,8 @@ export function ConfigPage() {
|
||||
const disableControls = connectionStatus !== 'connected';
|
||||
const isDirty = dirty || visualDirty;
|
||||
const hasVisualModeError = !!visualParseError;
|
||||
const hasVisualValidationErrors =
|
||||
activeTab === 'visual' && Object.values(visualValidationErrors).some(Boolean);
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -388,15 +391,23 @@ export function ConfigPage() {
|
||||
if (loading) return t('config_management.status_loading');
|
||||
if (error) return t('config_management.status_load_failed');
|
||||
if (hasVisualModeError) return t('config_management.visual_mode_unavailable');
|
||||
if (hasVisualValidationErrors) return t('config_management.visual.validation.validation_blocked');
|
||||
if (saving) return t('config_management.status_saving');
|
||||
if (isDirty) return t('config_management.status_dirty');
|
||||
return t('config_management.status_loaded');
|
||||
};
|
||||
|
||||
const isLoadedStatus = !disableControls && !loading && !error && !saving && !isDirty && !hasVisualModeError;
|
||||
const isLoadedStatus =
|
||||
!disableControls &&
|
||||
!loading &&
|
||||
!error &&
|
||||
!saving &&
|
||||
!isDirty &&
|
||||
!hasVisualModeError &&
|
||||
!hasVisualValidationErrors;
|
||||
|
||||
const getStatusClass = () => {
|
||||
if (error || hasVisualModeError) return styles.error;
|
||||
if (error || hasVisualModeError || hasVisualValidationErrors) return styles.error;
|
||||
if (isDirty) return styles.modified;
|
||||
if (!loading && !saving) return styles.saved;
|
||||
return '';
|
||||
@@ -438,7 +449,15 @@ export function ConfigPage() {
|
||||
type="button"
|
||||
className={styles.floatingActionButton}
|
||||
onClick={handleSave}
|
||||
disabled={disableControls || loading || saving || !isDirty || diffModalOpen || hasVisualModeError}
|
||||
disabled={
|
||||
disableControls ||
|
||||
loading ||
|
||||
saving ||
|
||||
!isDirty ||
|
||||
diffModalOpen ||
|
||||
hasVisualModeError ||
|
||||
hasVisualValidationErrors
|
||||
}
|
||||
title={t('config_management.save')}
|
||||
aria-label={t('config_management.save')}
|
||||
>
|
||||
@@ -485,6 +504,7 @@ export function ConfigPage() {
|
||||
{activeTab === 'visual' ? (
|
||||
<VisualConfigEditor
|
||||
values={visualValues}
|
||||
validationErrors={visualValidationErrors}
|
||||
disabled={disableControls || loading}
|
||||
onChange={setVisualValues}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
export type PayloadParamValueType = 'string' | 'number' | 'boolean' | 'json';
|
||||
|
||||
export type VisualConfigFieldPath =
|
||||
| 'port'
|
||||
| 'logsMaxTotalSizeMb'
|
||||
| 'requestRetry'
|
||||
| 'maxRetryInterval'
|
||||
| 'streaming.keepaliveSeconds'
|
||||
| 'streaming.bootstrapRetries'
|
||||
| 'streaming.nonstreamKeepaliveInterval';
|
||||
|
||||
export type VisualConfigValidationErrorCode = 'port_range' | 'non_negative_integer';
|
||||
|
||||
export type VisualConfigValidationErrors = Partial<
|
||||
Record<VisualConfigFieldPath, VisualConfigValidationErrorCode>
|
||||
>;
|
||||
|
||||
export type PayloadParamEntry = {
|
||||
id: string;
|
||||
path: string;
|
||||
|
||||
Reference in New Issue
Block a user