FEATURE (parsing): Add parsing connection string on DB creation

This commit is contained in:
Rostislav Dugin
2025-12-13 13:50:22 +03:00
parent db71a5ef7b
commit 9dac63430d
9 changed files with 1964 additions and 495 deletions

View File

@@ -82,6 +82,11 @@ jobs:
cd frontend
npm run lint
- name: Run frontend tests
run: |
cd frontend
npm run test
test-backend:
runs-on: ubuntu-latest
needs: [lint-backend]

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,9 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
@@ -22,6 +24,7 @@
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@vitest/coverage-v8": "^3.2.4",
"@eslint/js": "^9.25.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/react": "^19.1.2",
@@ -36,6 +39,7 @@
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
"vite": "^6.3.5",
"vitest": "^3.2.4"
}
}

View File

@@ -0,0 +1,528 @@
import { describe, expect, it } from 'vitest';
import {
ConnectionStringParser,
type ParseError,
type ParseResult,
} from './ConnectionStringParser';
describe('ConnectionStringParser', () => {
// Helper to assert successful parse
const expectSuccess = (result: ParseResult | ParseError): ParseResult => {
expect('error' in result).toBe(false);
return result as ParseResult;
};
// Helper to assert parse error
const expectError = (result: ParseResult | ParseError): ParseError => {
expect('error' in result).toBe(true);
return result as ParseError;
};
describe('Standard PostgreSQL URI (postgresql://)', () => {
it('should parse basic postgresql:// connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://myuser:mypassword@localhost:5432/mydb'),
);
expect(result.host).toBe('localhost');
expect(result.port).toBe(5432);
expect(result.username).toBe('myuser');
expect(result.password).toBe('mypassword');
expect(result.database).toBe('mydb');
expect(result.isHttps).toBe(false);
});
it('should default port to 5432 when not specified', () => {
const result = expectSuccess(ConnectionStringParser.parse('postgresql://user:pass@host/db'));
expect(result.port).toBe(5432);
});
it('should handle URL-encoded passwords', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://user:p%40ss%23word@host:5432/db'),
);
expect(result.password).toBe('p@ss#word');
});
it('should handle URL-encoded usernames', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://user%40domain:password@host:5432/db'),
);
expect(result.username).toBe('user@domain');
});
});
describe('Postgres URI (postgres://)', () => {
it('should parse basic postgres:// connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgres://admin:secret@db.example.com:5432/production'),
);
expect(result.host).toBe('db.example.com');
expect(result.port).toBe(5432);
expect(result.username).toBe('admin');
expect(result.password).toBe('secret');
expect(result.database).toBe('production');
});
});
describe('Supabase Direct Connection', () => {
it('should parse Supabase direct connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://postgres:mySecretPassword@db.abcdefghijklmnop.supabase.co:5432/postgres',
),
);
expect(result.host).toBe('db.abcdefghijklmnop.supabase.co');
expect(result.port).toBe(5432);
expect(result.username).toBe('postgres');
expect(result.password).toBe('mySecretPassword');
expect(result.database).toBe('postgres');
});
});
describe('Supabase Pooler Connection', () => {
it('should parse Supabase pooler session mode connection string (port 5432)', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgres://postgres.abcdefghijklmnop:myPassword@aws-0-us-east-1.pooler.supabase.com:5432/postgres',
),
);
expect(result.host).toBe('aws-0-us-east-1.pooler.supabase.com');
expect(result.port).toBe(5432);
expect(result.username).toBe('postgres.abcdefghijklmnop');
expect(result.password).toBe('myPassword');
expect(result.database).toBe('postgres');
});
it('should parse Supabase pooler transaction mode connection string (port 6543)', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgres://postgres.projectref:myPassword@aws-0-eu-west-1.pooler.supabase.com:6543/postgres',
),
);
expect(result.host).toBe('aws-0-eu-west-1.pooler.supabase.com');
expect(result.port).toBe(6543);
expect(result.username).toBe('postgres.projectref');
});
});
describe('JDBC Connection String', () => {
it('should parse JDBC connection string with user and password params', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'jdbc:postgresql://localhost:5432/mydb?user=admin&password=secret',
),
);
expect(result.host).toBe('localhost');
expect(result.port).toBe(5432);
expect(result.username).toBe('admin');
expect(result.password).toBe('secret');
expect(result.database).toBe('mydb');
});
it('should parse JDBC connection string without port', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'jdbc:postgresql://db.example.com/mydb?user=admin&password=secret',
),
);
expect(result.host).toBe('db.example.com');
expect(result.port).toBe(5432);
});
it('should parse JDBC with sslmode parameter', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'jdbc:postgresql://host:5432/db?user=u&password=p&sslmode=require',
),
);
expect(result.isHttps).toBe(true);
});
it('should return error for JDBC without user parameter', () => {
const result = expectError(
ConnectionStringParser.parse('jdbc:postgresql://host:5432/db?password=secret'),
);
expect(result.error).toContain('user');
expect(result.format).toBe('JDBC');
});
it('should return error for JDBC without password parameter', () => {
const result = expectError(
ConnectionStringParser.parse('jdbc:postgresql://host:5432/db?user=admin'),
);
expect(result.error).toContain('Password');
expect(result.format).toBe('JDBC');
});
});
describe('Neon Connection String', () => {
it('should parse Neon connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://neonuser:password123@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb',
),
);
expect(result.host).toBe('ep-cool-name-123456.us-east-2.aws.neon.tech');
expect(result.username).toBe('neonuser');
expect(result.database).toBe('neondb');
});
});
describe('Railway Connection String', () => {
it('should parse Railway connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://postgres:railwaypass@containers-us-west-123.railway.app:5432/railway',
),
);
expect(result.host).toBe('containers-us-west-123.railway.app');
expect(result.username).toBe('postgres');
expect(result.database).toBe('railway');
});
});
describe('Render Connection String', () => {
it('should parse Render connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://renderuser:renderpass@dpg-abc123.oregon-postgres.render.com/mydb',
),
);
expect(result.host).toBe('dpg-abc123.oregon-postgres.render.com');
expect(result.username).toBe('renderuser');
expect(result.database).toBe('mydb');
});
});
describe('DigitalOcean Connection String', () => {
it('should parse DigitalOcean connection string with sslmode', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://doadmin:dopassword@db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com:25060/defaultdb?sslmode=require',
),
);
expect(result.host).toBe('db-postgresql-nyc1-12345-do-user-123456-0.b.db.ondigitalocean.com');
expect(result.port).toBe(25060);
expect(result.username).toBe('doadmin');
expect(result.database).toBe('defaultdb');
expect(result.isHttps).toBe(true);
});
});
describe('AWS RDS Connection String', () => {
it('should parse AWS RDS connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://rdsuser:rdspass@mydb.abc123xyz.us-east-1.rds.amazonaws.com:5432/mydb',
),
);
expect(result.host).toBe('mydb.abc123xyz.us-east-1.rds.amazonaws.com');
expect(result.username).toBe('rdsuser');
});
});
describe('Azure Database for PostgreSQL Connection String', () => {
it('should parse Azure connection string with user@server format', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://myuser@myserver:mypassword@myserver.postgres.database.azure.com:5432/mydb?sslmode=require',
),
);
expect(result.host).toBe('myserver.postgres.database.azure.com');
expect(result.port).toBe(5432);
expect(result.username).toBe('myuser');
expect(result.password).toBe('mypassword');
expect(result.database).toBe('mydb');
expect(result.isHttps).toBe(true);
});
});
describe('Heroku Connection String', () => {
it('should parse Heroku connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgres://herokuuser:herokupass@ec2-12-34-56-789.compute-1.amazonaws.com:5432/herokudb',
),
);
expect(result.host).toBe('ec2-12-34-56-789.compute-1.amazonaws.com');
expect(result.username).toBe('herokuuser');
expect(result.database).toBe('herokudb');
});
});
describe('CockroachDB Connection String', () => {
it('should parse CockroachDB connection string with sslmode=verify-full', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://crdbuser:crdbpass@free-tier.gcp-us-central1.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full',
),
);
expect(result.host).toBe('free-tier.gcp-us-central1.cockroachlabs.cloud');
expect(result.port).toBe(26257);
expect(result.isHttps).toBe(true);
});
});
describe('SSL Mode Handling', () => {
it('should set isHttps=true for sslmode=require', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=require'),
);
expect(result.isHttps).toBe(true);
});
it('should set isHttps=true for sslmode=verify-ca', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=verify-ca'),
);
expect(result.isHttps).toBe(true);
});
it('should set isHttps=true for sslmode=verify-full', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=verify-full'),
);
expect(result.isHttps).toBe(true);
});
it('should set isHttps=false for sslmode=disable', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://u:p@host:5432/db?sslmode=disable'),
);
expect(result.isHttps).toBe(false);
});
it('should set isHttps=false when no sslmode specified', () => {
const result = expectSuccess(ConnectionStringParser.parse('postgresql://u:p@host:5432/db'));
expect(result.isHttps).toBe(false);
});
});
describe('libpq Key-Value Format', () => {
it('should parse libpq format connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'host=localhost port=5432 dbname=mydb user=admin password=secret',
),
);
expect(result.host).toBe('localhost');
expect(result.port).toBe(5432);
expect(result.username).toBe('admin');
expect(result.password).toBe('secret');
expect(result.database).toBe('mydb');
});
it('should parse libpq format with quoted password containing spaces', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
"host=localhost port=5432 dbname=mydb user=admin password='my secret pass'",
),
);
expect(result.password).toBe('my secret pass');
});
it('should default port to 5432 when not specified in libpq format', () => {
const result = expectSuccess(
ConnectionStringParser.parse('host=localhost dbname=mydb user=admin password=secret'),
);
expect(result.port).toBe(5432);
});
it('should handle hostaddr as alternative to host', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'hostaddr=192.168.1.1 port=5432 dbname=mydb user=admin password=secret',
),
);
expect(result.host).toBe('192.168.1.1');
});
it('should handle database as alternative to dbname', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'host=localhost port=5432 database=mydb user=admin password=secret',
),
);
expect(result.database).toBe('mydb');
});
it('should handle username as alternative to user', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'host=localhost port=5432 dbname=mydb username=admin password=secret',
),
);
expect(result.username).toBe('admin');
});
it('should parse sslmode in libpq format', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'host=localhost dbname=mydb user=admin password=secret sslmode=require',
),
);
expect(result.isHttps).toBe(true);
});
it('should return error for libpq format missing host', () => {
const result = expectError(
ConnectionStringParser.parse('port=5432 dbname=mydb user=admin password=secret'),
);
expect(result.error).toContain('Host');
expect(result.format).toBe('libpq');
});
it('should return error for libpq format missing user', () => {
const result = expectError(
ConnectionStringParser.parse('host=localhost dbname=mydb password=secret'),
);
expect(result.error).toContain('Username');
expect(result.format).toBe('libpq');
});
it('should return error for libpq format missing password', () => {
const result = expectError(
ConnectionStringParser.parse('host=localhost dbname=mydb user=admin'),
);
expect(result.error).toContain('Password');
expect(result.format).toBe('libpq');
});
it('should return error for libpq format missing dbname', () => {
const result = expectError(
ConnectionStringParser.parse('host=localhost user=admin password=secret'),
);
expect(result.error).toContain('Database');
expect(result.format).toBe('libpq');
});
});
describe('Error Cases', () => {
it('should return error for empty string', () => {
const result = expectError(ConnectionStringParser.parse(''));
expect(result.error).toContain('empty');
});
it('should return error for whitespace-only string', () => {
const result = expectError(ConnectionStringParser.parse(' '));
expect(result.error).toContain('empty');
});
it('should return error for unrecognized format', () => {
const result = expectError(ConnectionStringParser.parse('some random text'));
expect(result.error).toContain('Unrecognized');
});
it('should return error for missing username in URI', () => {
const result = expectError(
ConnectionStringParser.parse('postgresql://:password@host:5432/db'),
);
expect(result.error).toContain('Username');
});
it('should return error for missing password in URI', () => {
const result = expectError(ConnectionStringParser.parse('postgresql://user@host:5432/db'));
expect(result.error).toContain('Password');
});
it('should return error for missing database in URI', () => {
const result = expectError(ConnectionStringParser.parse('postgresql://user:pass@host:5432/'));
expect(result.error).toContain('Database');
});
it('should return error for invalid JDBC format', () => {
const result = expectError(ConnectionStringParser.parse('jdbc:postgresql://invalid'));
expect(result.format).toBe('JDBC');
});
});
describe('Edge Cases', () => {
it('should handle special characters in password', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://user:p%40ss%3Aw%2Ford@host:5432/db'),
);
expect(result.password).toBe('p@ss:w/ord');
});
it('should handle numeric database names', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://user:pass@host:5432/12345'),
);
expect(result.database).toBe('12345');
});
it('should handle hyphenated host names', () => {
const result = expectSuccess(
ConnectionStringParser.parse('postgresql://user:pass@my-database-host.example.com:5432/db'),
);
expect(result.host).toBe('my-database-host.example.com');
});
it('should handle connection string with extra query parameters', () => {
const result = expectSuccess(
ConnectionStringParser.parse(
'postgresql://user:pass@host:5432/db?sslmode=require&connect_timeout=10&application_name=myapp',
),
);
expect(result.isHttps).toBe(true);
expect(result.database).toBe('db');
});
it('should trim whitespace from connection string', () => {
const result = expectSuccess(
ConnectionStringParser.parse(' postgresql://user:pass@host:5432/db '),
);
expect(result.host).toBe('host');
});
});
});

View File

@@ -0,0 +1,284 @@
export type ParseResult = {
host: string;
port: number;
username: string;
password: string;
database: string;
isHttps: boolean;
};
export type ParseError = {
error: string;
format?: string;
};
export class ConnectionStringParser {
/**
* Parses a PostgreSQL connection string in various formats.
*
* Supported formats:
* 1. Standard PostgreSQL URI: postgresql://user:pass@host:port/db
* 2. Postgres URI: postgres://user:pass@host:port/db
* 3. Supabase Direct: postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres
* 4. Supabase Pooler Session: postgres://postgres.ref:pass@aws-0-region.pooler.supabase.com:5432/postgres
* 5. Supabase Pooler Transaction: same as above with port 6543
* 6. JDBC: jdbc:postgresql://host:port/db?user=x&password=y
* 7. Neon: postgresql://user:pass@ep-xxx.neon.tech/db
* 8. Railway: postgresql://postgres:pass@xxx.railway.app:port/railway
* 9. Render: postgresql://user:pass@xxx.render.com/db
* 10. DigitalOcean: postgresql://user:pass@xxx.ondigitalocean.com:port/db?sslmode=require
* 11. AWS RDS: postgresql://user:pass@xxx.rds.amazonaws.com:port/db
* 12. Azure: postgresql://user@server:pass@xxx.postgres.database.azure.com:port/db?sslmode=require
* 13. Heroku: postgres://user:pass@ec2-xxx.amazonaws.com:port/db
* 14. CockroachDB: postgresql://user:pass@xxx.cockroachlabs.cloud:port/db?sslmode=verify-full
* 15. With SSL params: postgresql://user:pass@host:port/db?sslmode=require
* 16. libpq key-value: host=x port=5432 dbname=db user=u password=p
*/
static parse(connectionString: string): ParseResult | ParseError {
const trimmed = connectionString.trim();
if (!trimmed) {
return { error: 'Connection string is empty' };
}
// Try JDBC format first (starts with jdbc:)
if (trimmed.startsWith('jdbc:postgresql://')) {
return this.parseJdbc(trimmed);
}
// Try libpq key-value format (contains key=value pairs without ://)
if (this.isLibpqFormat(trimmed)) {
return this.parseLibpq(trimmed);
}
// Try URI format (postgresql:// or postgres://)
if (trimmed.startsWith('postgresql://') || trimmed.startsWith('postgres://')) {
return this.parseUri(trimmed);
}
return {
error: 'Unrecognized connection string format',
};
}
private static isLibpqFormat(str: string): boolean {
// libpq format has key=value pairs separated by spaces
// Must contain at least host= or dbname= to be considered libpq format
return (
!str.includes('://') &&
(str.includes('host=') || str.includes('dbname=')) &&
str.includes('=')
);
}
private static parseUri(connectionString: string): ParseResult | ParseError {
try {
// Handle Azure format where username contains @: user@server:pass
// Azure format: postgresql://user@servername:password@host:port/db
const azureMatch = connectionString.match(
/^postgres(?:ql)?:\/\/([^@:]+)@([^:]+):([^@]+)@([^:/?]+):?(\d+)?\/([^?]+)(?:\?(.*))?$/,
);
if (azureMatch) {
const [, user, , password, host, port, database, queryString] = azureMatch;
const isHttps = this.checkSslMode(queryString);
return {
host: host,
port: port ? parseInt(port, 10) : 5432,
username: decodeURIComponent(user),
password: decodeURIComponent(password),
database: decodeURIComponent(database),
isHttps,
};
}
// Standard URI parsing using URL API
const url = new URL(connectionString);
const host = url.hostname;
const port = url.port ? parseInt(url.port, 10) : 5432;
const username = decodeURIComponent(url.username);
const password = decodeURIComponent(url.password);
const database = decodeURIComponent(url.pathname.slice(1)); // Remove leading /
const isHttps = this.checkSslMode(url.search);
// Validate required fields
if (!host) {
return { error: 'Host is missing from connection string' };
}
if (!username) {
return { error: 'Username is missing from connection string' };
}
if (!password) {
return { error: 'Password is missing from connection string' };
}
if (!database) {
return { error: 'Database name is missing from connection string' };
}
return {
host,
port,
username,
password,
database,
isHttps,
};
} catch (e) {
return {
error: `Failed to parse connection string: ${(e as Error).message}`,
format: 'URI',
};
}
}
private static parseJdbc(connectionString: string): ParseResult | ParseError {
try {
// JDBC format: jdbc:postgresql://host:port/database?user=x&password=y
const jdbcRegex = /^jdbc:postgresql:\/\/([^:/?]+):?(\d+)?\/([^?]+)(?:\?(.*))?$/;
const match = connectionString.match(jdbcRegex);
if (!match) {
return {
error:
'Invalid JDBC connection string format. Expected: jdbc:postgresql://host:port/database?user=x&password=y',
format: 'JDBC',
};
}
const [, host, port, database, queryString] = match;
if (!queryString) {
return {
error: 'JDBC connection string is missing query parameters (user and password)',
format: 'JDBC',
};
}
const params = new URLSearchParams(queryString);
const username = params.get('user');
const password = params.get('password');
const isHttps = this.checkSslMode(queryString);
if (!username) {
return {
error: 'Username (user parameter) is missing from JDBC connection string',
format: 'JDBC',
};
}
if (!password) {
return {
error: 'Password parameter is missing from JDBC connection string',
format: 'JDBC',
};
}
return {
host,
port: port ? parseInt(port, 10) : 5432,
username: decodeURIComponent(username),
password: decodeURIComponent(password),
database: decodeURIComponent(database),
isHttps,
};
} catch (e) {
return {
error: `Failed to parse JDBC connection string: ${(e as Error).message}`,
format: 'JDBC',
};
}
}
private static parseLibpq(connectionString: string): ParseResult | ParseError {
try {
// libpq format: host=x port=5432 dbname=db user=u password=p
// Values can be quoted with single quotes: password='my pass'
const params: Record<string, string> = {};
// Match key=value or key='quoted value'
const regex = /(\w+)=(?:'([^']*)'|(\S+))/g;
let match;
while ((match = regex.exec(connectionString)) !== null) {
const key = match[1];
const value = match[2] !== undefined ? match[2] : match[3];
params[key] = value;
}
const host = params['host'] || params['hostaddr'];
const port = params['port'];
const database = params['dbname'] || params['database'];
const username = params['user'] || params['username'];
const password = params['password'];
const sslmode = params['sslmode'];
if (!host) {
return {
error: 'Host is missing from connection string. Use host=hostname',
format: 'libpq',
};
}
if (!username) {
return {
error: 'Username is missing from connection string. Use user=username',
format: 'libpq',
};
}
if (!password) {
return {
error: 'Password is missing from connection string. Use password=yourpassword',
format: 'libpq',
};
}
if (!database) {
return {
error: 'Database name is missing from connection string. Use dbname=database',
format: 'libpq',
};
}
const isHttps = this.isSslEnabled(sslmode);
return {
host,
port: port ? parseInt(port, 10) : 5432,
username,
password,
database,
isHttps,
};
} catch (e) {
return {
error: `Failed to parse libpq connection string: ${(e as Error).message}`,
format: 'libpq',
};
}
}
private static checkSslMode(queryString: string | undefined | null): boolean {
if (!queryString) return false;
const params = new URLSearchParams(
queryString.startsWith('?') ? queryString.slice(1) : queryString,
);
const sslmode = params.get('sslmode');
return this.isSslEnabled(sslmode);
}
private static isSslEnabled(sslmode: string | null | undefined): boolean {
if (!sslmode) return false;
// These modes require SSL
const sslModes = ['require', 'verify-ca', 'verify-full'];
return sslModes.includes(sslmode.toLowerCase());
}
}

View File

@@ -1,8 +1,9 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button, Input, InputNumber, Select, Switch } from 'antd';
import { CopyOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { App, Button, Input, InputNumber, Select, Switch } from 'antd';
import { useEffect, useState } from 'react';
import { type Database, DatabaseType, databaseApi } from '../../../../entity/databases';
import { ConnectionStringParser } from '../../../../entity/databases/model/postgresql/ConnectionStringParser';
import { ToastHelper } from '../../../../shared/toast';
interface Props {
@@ -35,6 +36,8 @@ export const EditDatabaseSpecificDataComponent = ({
onSaved,
isShowDbName = true,
}: Props) => {
const { message } = App.useApp();
const [editingDatabase, setEditingDatabase] = useState<Database>();
const [isSaving, setIsSaving] = useState(false);
@@ -47,6 +50,46 @@ export const EditDatabaseSpecificDataComponent = ({
const [hasAutoAddedPublicSchema, setHasAutoAddedPublicSchema] = useState(false);
const parseFromClipboard = async () => {
try {
const text = await navigator.clipboard.readText();
const trimmedText = text.trim();
if (!trimmedText) {
message.error('Clipboard is empty');
return;
}
const result = ConnectionStringParser.parse(trimmedText);
if ('error' in result) {
message.error(result.error);
return;
}
if (!editingDatabase?.postgresql) return;
const updatedDatabase: Database = {
...editingDatabase,
postgresql: {
...editingDatabase.postgresql,
host: result.host,
port: result.port,
username: result.username,
password: result.password,
database: result.database,
isHttps: result.isHttps,
},
};
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
setIsConnectionTested(false);
message.success('Connection string parsed successfully');
} catch {
message.error('Failed to read clipboard. Please check browser permissions.');
}
};
const autoAddPublicSchemaForSupabase = (updatedDatabase: Database): Database => {
if (hasAutoAddedPublicSchema) return updatedDatabase;
@@ -140,6 +183,17 @@ export const EditDatabaseSpecificDataComponent = ({
<div>
{editingDatabase.type === DatabaseType.POSTGRES && (
<>
<div className="mb-3 flex">
<div className="min-w-[150px]" />
<div
className="cursor-pointer text-sm text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
onClick={parseFromClipboard}
>
<CopyOutlined className="mr-1" />
Parse from clipboard
</div>
</div>
<div className="mb-1 flex w-full items-center">
<div className="min-w-[150px]">Host</div>
<Input

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
});

470
package-lock.json generated
View File

@@ -1,470 +0,0 @@
{
"name": "postgresus",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@types/recharts": "^1.8.29",
"recharts": "^3.2.0"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "^1"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/recharts": {
"version": "1.8.29",
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
"integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
"license": "MIT",
"dependencies": {
"@types/d3-shape": "^1",
"@types/react": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/es-toolkit": {
"version": "1.39.10",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/immer": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.1"
}
},
"node_modules/react-is": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/recharts": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz",
"integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==",
"license": "MIT",
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT",
"peer": true
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/victory-vendor/node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
}
}
}

View File

@@ -1,6 +0,0 @@
{
"dependencies": {
"@types/recharts": "^1.8.29",
"recharts": "^3.2.0"
}
}