Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const ExternalServiceConnectionDetails: FC<FormPartProps> = ({ form }) =>
const selectedOption = formValues?.metricsParameters;
const urlValue = formValues?.url;
const portValidators = useMemo(() => [validators.required, Validators.validatePort], []);
// const timeoutValidators = useMemo(
// () => [Validators.duration, Validators.minDuration('0s'), Validators.durationUnit({ s: true, m: true })],
// []
// );

const trim = useCallback((value?: string) => (value ? value.trim() : value), []);
const getUrlParts = () => {
Expand Down Expand Up @@ -121,6 +125,14 @@ export const ExternalServiceConnectionDetails: FC<FormPartProps> = ({ form }) =>
tooltipText={Messages.form.tooltips.externalService.port}
validators={portValidators}
/>
{/* <TextInputField
key="timeout"
name="timeout"
label={Messages.form.labels.mainDetails.timeout}
tooltipText={Messages.form.tooltips.mainDetails.timeout}
placeholder={Messages.form.placeholders.mainDetails.timeout}
validators={timeoutValidators}
/> */}
<div />
</div>
<div className={styles.group}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const Messages = {
username: 'Username',
password: 'Password',
instanceID: 'Instance ID',
timeout: 'Connection timeout',
},
postgresqlDetails: {
database: 'Database',
Expand Down Expand Up @@ -97,6 +98,7 @@ export const Messages = {
username: 'Username',
password: 'Password',
instanceID: 'Instance ID',
timeout: 'Connection timeout',
},
postgresqlDetails: {
database: 'Database (default: postgres)',
Expand Down Expand Up @@ -140,6 +142,8 @@ export const Messages = {
username: 'Your database user name',
password: 'Your database password',
instanceID: 'Instance ID to use',
// todo: update tooltip
timeout: 'Connection timeout for the database connection (e.g. 10s, 500ms, 5m)',
},
postgresqlDetails: {
database: 'Database name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const MainDetailsFormPart: FC<MainDetailsFormPartProps> = ({ form, type,
const tlsFlag = formValues && formValues['tls'];

const portValidators = useMemo(() => [validators.required, Validators.validatePort], []);
// const timeoutValidators = useMemo(
// () => [Validators.duration, Validators.minDuration('0s'), Validators.durationUnit({ s: true, m: true })],
// []
// );
const userPassValidators = useMemo(
() => (tlsFlag || type === Databases.valkey ? [] : [validators.required]),
[tlsFlag, type]
Expand Down Expand Up @@ -71,6 +75,17 @@ export const MainDetailsFormPart: FC<MainDetailsFormPartProps> = ({ form, type,
validators={userPassValidators}
/>
</div>
{/* <div className={styles.group}>
<TextInputField
key="timeout"
name="timeout"
label={Messages.form.labels.mainDetails.timeout}
tooltipText={Messages.form.tooltips.mainDetails.timeout}
placeholder={Messages.form.placeholders.mainDetails.timeout}
validators={timeoutValidators}
/>
<div />
</div> */}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const MongoDBConnectionDetails: FC<MainDetailsFormPartProps> = ({ form, r
const tlsFlag = formValues && formValues['tls'];

const portValidators = useMemo(() => [validators.required, Validators.validatePort], []);
// const timeoutValidators = useMemo(
// () => [Validators.duration, Validators.minDuration('0s'), Validators.durationUnit({ s: true, m: true })],
// []
// );
const userPassValidators = useMemo(() => (tlsFlag ? [] : [validators.required]), [tlsFlag]);
const maxQueryLengthValidators = useMemo(() => [Validators.min(-1)], []);

Expand Down Expand Up @@ -77,6 +81,14 @@ export const MongoDBConnectionDetails: FC<MainDetailsFormPartProps> = ({ form, r
placeholder={Messages.form.placeholders.mongodbDetails.maxQueryLength}
validators={maxQueryLengthValidators}
/>
{/* <TextInputField
key="timeout"
name="timeout"
label={Messages.form.labels.mainDetails.timeout}
tooltipText={Messages.form.tooltips.mainDetails.timeout}
placeholder={Messages.form.placeholders.mainDetails.timeout}
validators={timeoutValidators}
/> */}
<div />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const MySQLConnectionDetails: FC<MainDetailsFormPartProps> = ({ form, rem
const portValidators = useMemo(() => [validators.required, Validators.validatePort], []);
const userPassValidators = useMemo(() => (tlsFlag ? [] : [validators.required]), [tlsFlag]);
const maxQueryLengthValidators = useMemo(() => [Validators.min(-1)], []);
// const timeoutValidators = useMemo(
// () => [Validators.duration, Validators.minDuration('0s'), Validators.durationUnit({ s: true, m: true })],
// []
// );

return (
<div className={styles.groupWrapper}>
Expand Down Expand Up @@ -87,6 +91,14 @@ export const MySQLConnectionDetails: FC<MainDetailsFormPartProps> = ({ form, rem
placeholder={Messages.form.placeholders.mysqlDetails.maxQueryLength}
validators={maxQueryLengthValidators}
/>
{/* <TextInputField
key="timeout"
name="timeout"
label={Messages.form.labels.mainDetails.timeout}
tooltipText={Messages.form.tooltips.mainDetails.timeout}
placeholder={Messages.form.placeholders.mainDetails.timeout}
validators={timeoutValidators}
/> */}
<div />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const PostgreSQLConnectionDetails: FC<MainDetailsFormPartProps> = ({ form
const portValidators = useMemo(() => [validators.required, Validators.validatePort], []);
const userPassValidators = useMemo(() => (tlsFlag ? [] : [validators.required]), [tlsFlag]);
const maxQueryLengthValidators = useMemo(() => [Validators.min(-1)], []);
const timeoutValidators = useMemo(
() => [Validators.duration, Validators.minDuration('0s'), Validators.durationUnit({ s: true, m: true })],
[]
);

return (
<div className={styles.groupWrapper}>
Expand Down Expand Up @@ -94,6 +98,17 @@ export const PostgreSQLConnectionDetails: FC<MainDetailsFormPartProps> = ({ form
tooltipText={Messages.form.tooltips.postgresqlDetails.maxQueryLength}
/>
</div>
<div className={styles.group}>
<TextInputField
key="timeout"
name="timeout"
label={Messages.form.labels.mainDetails.timeout}
tooltipText={Messages.form.tooltips.mainDetails.timeout}
placeholder={Messages.form.placeholders.mainDetails.timeout}
validators={timeoutValidators}
/>
<div />
</div>
</div>
);
};
1 change: 1 addition & 0 deletions public/app/percona/add-instance/panel.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface RemoteInstanceCredentials {
tls?: boolean;
tls_skip_verify?: boolean;
pmm_agent_id?: string;
timeout?: string;
}

export enum InstanceTypesExtra {
Expand Down
100 changes: 100 additions & 0 deletions public/app/percona/shared/helpers/duration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { durationToMs, isValidProtobufDuration } from './duration';

describe('isValidProtobufDuration', () => {
it('should accept integer seconds', () => {
expect(isValidProtobufDuration('1s')).toBe(true);
expect(isValidProtobufDuration('60s')).toBe(true);
expect(isValidProtobufDuration('0s')).toBe(true);
});

it('should accept fractional seconds', () => {
expect(isValidProtobufDuration('1.5s')).toBe(true);
expect(isValidProtobufDuration('0.001s')).toBe(true);
});

it('should accept negative durations', () => {
expect(isValidProtobufDuration('-1s')).toBe(true);
expect(isValidProtobufDuration('-1.5s')).toBe(true);
});

it('should accept millisecond notation', () => {
expect(isValidProtobufDuration('500ms')).toBe(true);
expect(isValidProtobufDuration('1ms')).toBe(true);
expect(isValidProtobufDuration('0ms')).toBe(true);
expect(isValidProtobufDuration('1.5ms')).toBe(true);
});

it('should trim whitespace', () => {
expect(isValidProtobufDuration(' 1s ')).toBe(true);
expect(isValidProtobufDuration(' 500ms ')).toBe(true);
});

it('should reject bare numbers', () => {
expect(isValidProtobufDuration('1')).toBe(false);
});

it('should reject empty string', () => {
expect(isValidProtobufDuration('')).toBe(false);
});

it('should accept minute notation', () => {
expect(isValidProtobufDuration('1m')).toBe(true);
expect(isValidProtobufDuration('0m')).toBe(true);
expect(isValidProtobufDuration('60m')).toBe(true);
expect(isValidProtobufDuration('0.5m')).toBe(true);
expect(isValidProtobufDuration('-1m')).toBe(true);
});

it('should reject other units', () => {
expect(isValidProtobufDuration('1h')).toBe(false);
});
});

describe('durationToMs', () => {
it('should convert seconds to milliseconds', () => {
expect(durationToMs('1s')).toBe(1000);
expect(durationToMs('2s')).toBe(2000);
expect(durationToMs('0s')).toBe(0);
});

it('should convert fractional seconds to milliseconds', () => {
expect(durationToMs('1.5s')).toBe(1500);
expect(durationToMs('0.5s')).toBe(500);
});

it('should convert milliseconds', () => {
expect(durationToMs('500ms')).toBe(500);
expect(durationToMs('1ms')).toBe(1);
expect(durationToMs('0ms')).toBe(0);
});

it('should convert fractional milliseconds', () => {
expect(durationToMs('1.5ms')).toBe(1.5);
});

it('should trim whitespace', () => {
expect(durationToMs(' 1s ')).toBe(1000);
expect(durationToMs(' 500ms ')).toBe(500);
});

it('should convert minutes to milliseconds', () => {
expect(durationToMs('1m')).toBe(60000);
expect(durationToMs('2m')).toBe(120000);
expect(durationToMs('0m')).toBe(0);
});

it('should convert fractional minutes to milliseconds', () => {
expect(durationToMs('0.5m')).toBe(30000);
expect(durationToMs('1.5m')).toBe(90000);
});

it('should trim whitespace for minutes', () => {
expect(durationToMs(' 1m ')).toBe(60000);
});

it('should throw on invalid input', () => {
expect(() => durationToMs('1h')).toThrow('Invalid duration: "1h"');
expect(() => durationToMs('1')).toThrow('Invalid duration: "1"');
expect(() => durationToMs('')).toThrow();
});
});
34 changes: 34 additions & 0 deletions public/app/percona/shared/helpers/duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UnitOptions } from './validator.types';

export const isValidProtobufDuration = (durationString: string): boolean =>
/^-?[0-9]+(\.[0-9]+)?(ms|s|m)$/.test(durationString.trim());

export const durationToMs = (duration: string): number => {
const trimmed = duration.trim();

if (trimmed.endsWith('ms')) {
return parseFloat(trimmed.slice(0, -2));
}

if (trimmed.endsWith('m')) {
return parseFloat(trimmed.slice(0, -1)) * 60 * 1000;
}

if (trimmed.endsWith('s')) {
return parseFloat(trimmed.slice(0, -1)) * 1000;
}

throw new Error(`Invalid duration: "${duration}"`);
};

export const hasValidUnit = (duration: string, options: UnitOptions): boolean => {
const unit = getDurationUnit(duration);
return !!unit && !!options[unit as keyof UnitOptions];
};

const DURATION_RE = /^-?\d+(?:\.\d+)?(ms|s|m)$/;

export const getDurationUnit = (value: string): string | undefined => {
const match = value.trim().match(DURATION_RE);
return match?.[1];
};
6 changes: 6 additions & 0 deletions public/app/percona/shared/helpers/validator.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type VResult = string | undefined;

export type Validator = (value: any, values?: Record<string, any>, meta?: any) => VResult;

export type UnitOptions = {
ms?: boolean;
s?: boolean;
m?: boolean;
};
74 changes: 74 additions & 0 deletions public/app/percona/shared/helpers/validators.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,78 @@ describe('validators compose', () => {

expect(validate(120, {})).toEqual(errorMessage);
});

describe('validate duration', () => {
it('return undefined when value is valid', () => {
expect(validators.duration('1s')).toBeUndefined();
expect(validators.duration('1ms')).toBeUndefined();
expect(validators.duration('1.5s')).toBeUndefined();
expect(validators.duration('1.5ms')).toBeUndefined();
expect(validators.duration('1m')).toBeUndefined();
expect(validators.duration('0.5m')).toBeUndefined();
expect(validators.duration('60m')).toBeUndefined();
});

it('return error message when value is invalid', () => {
expect(validators.duration('1')).toEqual('Invalid duration');
expect(validators.duration('1m.1s')).toEqual('Invalid duration');
expect(validators.duration('1h.1m')).toEqual('Invalid duration');
expect(validators.duration('1d.1h')).toEqual('Invalid duration');
expect(validators.duration('1w.1d')).toEqual('Invalid duration');
expect(validators.duration('1y.1w')).toEqual('Invalid duration');
expect(validators.duration('1h')).toEqual('Invalid duration');
});
});

describe('validate min duration', () => {
it('return undefined when value is valid', () => {
expect(validators.minDuration('1s')('1s')).toBeUndefined();
expect(validators.minDuration('1ms')('1ms')).toBeUndefined();
expect(validators.minDuration('1m')('1m')).toBeUndefined();
expect(validators.minDuration('1m')('2m')).toBeUndefined();
expect(validators.minDuration('30s')('1m')).toBeUndefined();
expect(validators.minDuration('1m')('90s')).toBeUndefined();
});

it('return error message when value is invalid', () => {
expect(validators.minDuration('1s')('0s')).toEqual('Duration should be greater or equal to 1s');
expect(validators.minDuration('1ms')('0s')).toEqual('Duration should be greater or equal to 1ms');
expect(validators.minDuration('1m')('30s')).toEqual('Duration should be greater or equal to 1m');
expect(validators.minDuration('2m')('1m')).toEqual('Duration should be greater or equal to 2m');
});
});

describe('validate duration unit', () => {
it('return undefined when value is valid', () => {
const values = {
ms: '1ms',
s: '1s',
m: '1m',
};

for (const [unit, value] of Object.entries(values)) {
expect(validators.durationUnit({ [unit]: true })(value)).toBeUndefined();

for (const [otherUnit, otherValue] of Object.entries(values)) {
if (unit !== otherUnit) {
expect(validators.durationUnit({ [unit]: true })(otherValue)).toEqual(
`Invalid unit. Allowed units: ${unit}`
);
}
}
}
});

it('allows only s and m when configured', () => {
const validator = validators.durationUnit({ s: true, m: true, ms: false });

expect(validator('1s')).toBeUndefined();
expect(validator('1m')).toBeUndefined();
expect(validator('1ms')).toEqual('Invalid unit. Allowed units: s, m');
});

it('returns invalid duration when format is wrong', () => {
expect(validators.durationUnit({ s: true, m: true })('1')).toEqual('Invalid duration');
});
});
});
Loading
Loading