mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (clipboard): Add parsing from clipboard via dialog in HTTP\no navigator mode
This commit is contained in:
@@ -1566,6 +1566,13 @@ export const ReactComponent = ({ someValue }: Props): JSX.Element => {
|
|||||||
- **Calculated values** - Derived data from props/state
|
- **Calculated values** - Derived data from props/state
|
||||||
- **Return** - JSX markup
|
- **Return** - JSX markup
|
||||||
|
|
||||||
|
### Clipboard operations
|
||||||
|
|
||||||
|
Always use `ClipboardHelper` (`shared/lib/ClipboardHelper.ts`) for clipboard operations — never call `navigator.clipboard` directly.
|
||||||
|
|
||||||
|
- **Copy:** `ClipboardHelper.copyToClipboard(text)` — uses `navigator.clipboard` with `execCommand('copy')` fallback for non-secure contexts (HTTP).
|
||||||
|
- **Paste:** Check `ClipboardHelper.isClipboardApiAvailable()` first. If available, use `ClipboardHelper.readFromClipboard()`. If not, show `ClipboardPasteModalComponent` (`shared/ui`) which lets the user paste manually via a text input modal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package secrets
|
|
||||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
|||||||
import { getApplicationServer } from '../../../constants';
|
import { getApplicationServer } from '../../../constants';
|
||||||
import { type Backup, PgWalBackupType } from '../../../entity/backups';
|
import { type Backup, PgWalBackupType } from '../../../entity/backups';
|
||||||
import { type Database } from '../../../entity/databases';
|
import { type Database } from '../../../entity/databases';
|
||||||
|
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||||
import { getUserTimeFormat } from '../../../shared/time';
|
import { getUserTimeFormat } from '../../../shared/time';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,7 +27,7 @@ export const AgentRestoreComponent = ({ database, backup }: Props) => {
|
|||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await ClipboardHelper.copyToClipboard(text);
|
||||||
message.success('Copied to clipboard');
|
message.success('Copied to clipboard');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to copy');
|
message.error('Failed to copy');
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||||
|
|
||||||
interface DbSizeCommand {
|
interface DbSizeCommand {
|
||||||
label: string;
|
label: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -44,7 +46,7 @@ export function DbSizeCommands({ commands }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(cmd.code);
|
await ClipboardHelper.copyToClipboard(cmd.code);
|
||||||
setCopiedIndex(index);
|
setCopiedIndex(index);
|
||||||
setTimeout(() => setCopiedIndex(null), 2000);
|
setTimeout(() => setCopiedIndex(null), 2000);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { getApplicationServer } from '../../../constants';
|
import { getApplicationServer } from '../../../constants';
|
||||||
import { type Database, databaseApi } from '../../../entity/databases';
|
import { type Database, databaseApi } from '../../../entity/databases';
|
||||||
|
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||||
|
|
||||||
type Architecture = 'amd64' | 'arm64';
|
type Architecture = 'amd64' | 'arm64';
|
||||||
type PgDeploymentType = 'system' | 'folder' | 'docker';
|
type PgDeploymentType = 'system' | 'folder' | 'docker';
|
||||||
@@ -42,7 +43,7 @@ export const AgentInstallationComponent = ({ database, onTokenGenerated }: Props
|
|||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await ClipboardHelper.copyToClipboard(text);
|
||||||
message.success('Copied to clipboard');
|
message.success('Copied to clipboard');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to copy');
|
message.error('Failed to copy');
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { IS_CLOUD } from '../../../../constants';
|
import { IS_CLOUD } from '../../../../constants';
|
||||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||||
import { MariadbConnectionStringParser } from '../../../../entity/databases/model/mariadb/MariadbConnectionStringParser';
|
import { MariadbConnectionStringParser } from '../../../../entity/databases/model/mariadb/MariadbConnectionStringParser';
|
||||||
|
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||||
import { ToastHelper } from '../../../../shared/toast';
|
import { ToastHelper } from '../../../../shared/toast';
|
||||||
|
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
database: Database;
|
database: Database;
|
||||||
@@ -49,41 +51,52 @@ export const EditMariaDbSpecificDataComponent = ({
|
|||||||
const hasAdvancedValues = !!database.mariadb?.isExcludeEvents;
|
const hasAdvancedValues = !!database.mariadb?.isExcludeEvents;
|
||||||
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||||
|
|
||||||
|
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||||
|
|
||||||
|
const applyConnectionString = (text: string) => {
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
|
||||||
|
if (!trimmedText) {
|
||||||
|
message.error('Clipboard is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = MariadbConnectionStringParser.parse(trimmedText);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
message.error(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editingDatabase?.mariadb) return;
|
||||||
|
|
||||||
|
const updatedDatabase: Database = {
|
||||||
|
...editingDatabase,
|
||||||
|
mariadb: {
|
||||||
|
...editingDatabase.mariadb,
|
||||||
|
host: result.host,
|
||||||
|
port: result.port,
|
||||||
|
username: result.username,
|
||||||
|
password: result.password,
|
||||||
|
database: result.database,
|
||||||
|
isHttps: result.isHttps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditingDatabase(updatedDatabase);
|
||||||
|
setIsConnectionTested(false);
|
||||||
|
message.success('Connection string parsed successfully');
|
||||||
|
};
|
||||||
|
|
||||||
const parseFromClipboard = async () => {
|
const parseFromClipboard = async () => {
|
||||||
|
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||||
|
setIsShowPasteModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await ClipboardHelper.readFromClipboard();
|
||||||
const trimmedText = text.trim();
|
applyConnectionString(text);
|
||||||
|
|
||||||
if (!trimmedText) {
|
|
||||||
message.error('Clipboard is empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = MariadbConnectionStringParser.parse(trimmedText);
|
|
||||||
|
|
||||||
if ('error' in result) {
|
|
||||||
message.error(result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editingDatabase?.mariadb) return;
|
|
||||||
|
|
||||||
const updatedDatabase: Database = {
|
|
||||||
...editingDatabase,
|
|
||||||
mariadb: {
|
|
||||||
...editingDatabase.mariadb,
|
|
||||||
host: result.host,
|
|
||||||
port: result.port,
|
|
||||||
username: result.username,
|
|
||||||
password: result.password,
|
|
||||||
database: result.database,
|
|
||||||
isHttps: result.isHttps,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setEditingDatabase(updatedDatabase);
|
|
||||||
setIsConnectionTested(false);
|
|
||||||
message.success('Connection string parsed successfully');
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||||
}
|
}
|
||||||
@@ -408,6 +421,15 @@ export const EditMariaDbSpecificDataComponent = ({
|
|||||||
list.
|
list.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ClipboardPasteModalComponent
|
||||||
|
open={isShowPasteModal}
|
||||||
|
onSubmit={(text) => {
|
||||||
|
setIsShowPasteModal(false);
|
||||||
|
applyConnectionString(text);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsShowPasteModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { IS_CLOUD } from '../../../../constants';
|
import { IS_CLOUD } from '../../../../constants';
|
||||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||||
import { MongodbConnectionStringParser } from '../../../../entity/databases/model/mongodb/MongodbConnectionStringParser';
|
import { MongodbConnectionStringParser } from '../../../../entity/databases/model/mongodb/MongodbConnectionStringParser';
|
||||||
|
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||||
import { ToastHelper } from '../../../../shared/toast';
|
import { ToastHelper } from '../../../../shared/toast';
|
||||||
|
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
database: Database;
|
database: Database;
|
||||||
@@ -52,56 +54,65 @@ export const EditMongoDbSpecificDataComponent = ({
|
|||||||
!!database.mongodb?.isDirectConnection;
|
!!database.mongodb?.isDirectConnection;
|
||||||
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
|
||||||
|
|
||||||
|
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||||
|
|
||||||
|
const applyConnectionString = (text: string) => {
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
|
||||||
|
if (!trimmedText) {
|
||||||
|
message.error('Clipboard is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = MongodbConnectionStringParser.parse(trimmedText);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
message.error(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editingDatabase?.mongodb) return;
|
||||||
|
|
||||||
|
const updatedDatabase: Database = {
|
||||||
|
...editingDatabase,
|
||||||
|
mongodb: {
|
||||||
|
...editingDatabase.mongodb,
|
||||||
|
host: result.host,
|
||||||
|
port: result.port,
|
||||||
|
username: result.username,
|
||||||
|
password: result.password || '',
|
||||||
|
database: result.database,
|
||||||
|
authDatabase: result.authDatabase,
|
||||||
|
isHttps: result.useTls,
|
||||||
|
isSrv: result.isSrv,
|
||||||
|
isDirectConnection: result.isDirectConnection,
|
||||||
|
cpuCount: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.isSrv || result.isDirectConnection) {
|
||||||
|
setShowAdvanced(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingDatabase(updatedDatabase);
|
||||||
|
setIsConnectionTested(false);
|
||||||
|
|
||||||
|
if (!result.password) {
|
||||||
|
message.warning('Connection string parsed successfully. Please enter the password manually.');
|
||||||
|
} else {
|
||||||
|
message.success('Connection string parsed successfully');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const parseFromClipboard = async () => {
|
const parseFromClipboard = async () => {
|
||||||
|
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||||
|
setIsShowPasteModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await ClipboardHelper.readFromClipboard();
|
||||||
const trimmedText = text.trim();
|
applyConnectionString(text);
|
||||||
|
|
||||||
if (!trimmedText) {
|
|
||||||
message.error('Clipboard is empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = MongodbConnectionStringParser.parse(trimmedText);
|
|
||||||
|
|
||||||
if ('error' in result) {
|
|
||||||
message.error(result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editingDatabase?.mongodb) return;
|
|
||||||
|
|
||||||
const updatedDatabase: Database = {
|
|
||||||
...editingDatabase,
|
|
||||||
mongodb: {
|
|
||||||
...editingDatabase.mongodb,
|
|
||||||
host: result.host,
|
|
||||||
port: result.port,
|
|
||||||
username: result.username,
|
|
||||||
password: result.password || '',
|
|
||||||
database: result.database,
|
|
||||||
authDatabase: result.authDatabase,
|
|
||||||
isHttps: result.useTls,
|
|
||||||
isSrv: result.isSrv,
|
|
||||||
isDirectConnection: result.isDirectConnection,
|
|
||||||
cpuCount: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result.isSrv || result.isDirectConnection) {
|
|
||||||
setShowAdvanced(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditingDatabase(updatedDatabase);
|
|
||||||
setIsConnectionTested(false);
|
|
||||||
|
|
||||||
if (!result.password) {
|
|
||||||
message.warning(
|
|
||||||
'Connection string parsed successfully. Please enter the password manually.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
message.success('Connection string parsed successfully');
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||||
}
|
}
|
||||||
@@ -501,6 +512,15 @@ export const EditMongoDbSpecificDataComponent = ({
|
|||||||
list.
|
list.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ClipboardPasteModalComponent
|
||||||
|
open={isShowPasteModal}
|
||||||
|
onSubmit={(text) => {
|
||||||
|
setIsShowPasteModal(false);
|
||||||
|
applyConnectionString(text);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsShowPasteModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { IS_CLOUD } from '../../../../constants';
|
import { IS_CLOUD } from '../../../../constants';
|
||||||
import { type Database, databaseApi } from '../../../../entity/databases';
|
import { type Database, databaseApi } from '../../../../entity/databases';
|
||||||
import { MySqlConnectionStringParser } from '../../../../entity/databases/model/mysql/MySqlConnectionStringParser';
|
import { MySqlConnectionStringParser } from '../../../../entity/databases/model/mysql/MySqlConnectionStringParser';
|
||||||
|
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||||
import { ToastHelper } from '../../../../shared/toast';
|
import { ToastHelper } from '../../../../shared/toast';
|
||||||
|
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
database: Database;
|
database: Database;
|
||||||
@@ -46,41 +48,52 @@ export const EditMySqlSpecificDataComponent = ({
|
|||||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||||
const [isConnectionFailed, setIsConnectionFailed] = useState(false);
|
const [isConnectionFailed, setIsConnectionFailed] = useState(false);
|
||||||
|
|
||||||
|
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||||
|
|
||||||
|
const applyConnectionString = (text: string) => {
|
||||||
|
const trimmedText = text.trim();
|
||||||
|
|
||||||
|
if (!trimmedText) {
|
||||||
|
message.error('Clipboard is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = MySqlConnectionStringParser.parse(trimmedText);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
message.error(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editingDatabase?.mysql) return;
|
||||||
|
|
||||||
|
const updatedDatabase: Database = {
|
||||||
|
...editingDatabase,
|
||||||
|
mysql: {
|
||||||
|
...editingDatabase.mysql,
|
||||||
|
host: result.host,
|
||||||
|
port: result.port,
|
||||||
|
username: result.username,
|
||||||
|
password: result.password,
|
||||||
|
database: result.database,
|
||||||
|
isHttps: result.isHttps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditingDatabase(updatedDatabase);
|
||||||
|
setIsConnectionTested(false);
|
||||||
|
message.success('Connection string parsed successfully');
|
||||||
|
};
|
||||||
|
|
||||||
const parseFromClipboard = async () => {
|
const parseFromClipboard = async () => {
|
||||||
|
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||||
|
setIsShowPasteModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await ClipboardHelper.readFromClipboard();
|
||||||
const trimmedText = text.trim();
|
applyConnectionString(text);
|
||||||
|
|
||||||
if (!trimmedText) {
|
|
||||||
message.error('Clipboard is empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = MySqlConnectionStringParser.parse(trimmedText);
|
|
||||||
|
|
||||||
if ('error' in result) {
|
|
||||||
message.error(result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editingDatabase?.mysql) return;
|
|
||||||
|
|
||||||
const updatedDatabase: Database = {
|
|
||||||
...editingDatabase,
|
|
||||||
mysql: {
|
|
||||||
...editingDatabase.mysql,
|
|
||||||
host: result.host,
|
|
||||||
port: result.port,
|
|
||||||
username: result.username,
|
|
||||||
password: result.password,
|
|
||||||
database: result.database,
|
|
||||||
isHttps: result.isHttps,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setEditingDatabase(updatedDatabase);
|
|
||||||
setIsConnectionTested(false);
|
|
||||||
message.success('Connection string parsed successfully');
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||||
}
|
}
|
||||||
@@ -359,6 +372,15 @@ export const EditMySqlSpecificDataComponent = ({
|
|||||||
list.
|
list.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ClipboardPasteModalComponent
|
||||||
|
open={isShowPasteModal}
|
||||||
|
onSubmit={(text) => {
|
||||||
|
setIsShowPasteModal(false);
|
||||||
|
applyConnectionString(text);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsShowPasteModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { IS_CLOUD } from '../../../../constants';
|
import { IS_CLOUD } from '../../../../constants';
|
||||||
import { type Database, PostgresBackupType, databaseApi } from '../../../../entity/databases';
|
import { type Database, PostgresBackupType, databaseApi } from '../../../../entity/databases';
|
||||||
import { ConnectionStringParser } from '../../../../entity/databases/model/postgresql/ConnectionStringParser';
|
import { ConnectionStringParser } from '../../../../entity/databases/model/postgresql/ConnectionStringParser';
|
||||||
|
import { ClipboardHelper } from '../../../../shared/lib/ClipboardHelper';
|
||||||
import { ToastHelper } from '../../../../shared/toast';
|
import { ToastHelper } from '../../../../shared/toast';
|
||||||
|
import { ClipboardPasteModalComponent } from '../../../../shared/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
database: Database;
|
database: Database;
|
||||||
@@ -54,42 +56,53 @@ export const EditPostgreSqlSpecificDataComponent = ({
|
|||||||
|
|
||||||
const [hasAutoAddedPublicSchema, setHasAutoAddedPublicSchema] = useState(false);
|
const [hasAutoAddedPublicSchema, setHasAutoAddedPublicSchema] = useState(false);
|
||||||
|
|
||||||
|
const [isShowPasteModal, setIsShowPasteModal] = useState(false);
|
||||||
|
|
||||||
|
const applyConnectionString = (text: string) => {
|
||||||
|
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,
|
||||||
|
cpuCount: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
|
||||||
|
setIsConnectionTested(false);
|
||||||
|
message.success('Connection string parsed successfully');
|
||||||
|
};
|
||||||
|
|
||||||
const parseFromClipboard = async () => {
|
const parseFromClipboard = async () => {
|
||||||
|
if (!ClipboardHelper.isClipboardApiAvailable()) {
|
||||||
|
setIsShowPasteModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await ClipboardHelper.readFromClipboard();
|
||||||
const trimmedText = text.trim();
|
applyConnectionString(text);
|
||||||
|
|
||||||
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,
|
|
||||||
cpuCount: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setEditingDatabase(autoAddPublicSchemaForSupabase(updatedDatabase));
|
|
||||||
setIsConnectionTested(false);
|
|
||||||
message.success('Connection string parsed successfully');
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Failed to read clipboard. Please check browser permissions.');
|
message.error('Failed to read clipboard. Please check browser permissions.');
|
||||||
}
|
}
|
||||||
@@ -603,6 +616,15 @@ export const EditPostgreSqlSpecificDataComponent = ({
|
|||||||
<div>
|
<div>
|
||||||
{renderBackupTypeSelector()}
|
{renderBackupTypeSelector()}
|
||||||
{renderFormContent()}
|
{renderFormContent()}
|
||||||
|
|
||||||
|
<ClipboardPasteModalComponent
|
||||||
|
open={isShowPasteModal}
|
||||||
|
onSubmit={(text) => {
|
||||||
|
setIsShowPasteModal(false);
|
||||||
|
applyConnectionString(text);
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsShowPasteModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import type { Backup } from '../../../entity/backups';
|
import type { Backup } from '../../../entity/backups';
|
||||||
import { type Database, DatabaseType } from '../../../entity/databases';
|
import { type Database, DatabaseType } from '../../../entity/databases';
|
||||||
import { type Restore, RestoreStatus, restoreApi } from '../../../entity/restores';
|
import { type Restore, RestoreStatus, restoreApi } from '../../../entity/restores';
|
||||||
|
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||||
import { getUserTimeFormat } from '../../../shared/time';
|
import { getUserTimeFormat } from '../../../shared/time';
|
||||||
import { ConfirmationComponent } from '../../../shared/ui';
|
import { ConfirmationComponent } from '../../../shared/ui';
|
||||||
import { EditDatabaseSpecificDataComponent } from '../../databases/ui/edit/EditDatabaseSpecificDataComponent';
|
import { EditDatabaseSpecificDataComponent } from '../../databases/ui/edit/EditDatabaseSpecificDataComponent';
|
||||||
@@ -328,7 +329,7 @@ export const RestoresComponent = ({ database, backup }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(showingRestoreError.failMessage || '');
|
ClipboardHelper.copyToClipboard(showingRestoreError.failMessage || '');
|
||||||
message.success('Error message copied to clipboard');
|
message.success('Error message copied to clipboard');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import { IS_CLOUD, getApplicationServer } from '../../../constants';
|
import { IS_CLOUD, getApplicationServer } from '../../../constants';
|
||||||
import { settingsApi } from '../../../entity/users/api/settingsApi';
|
import { settingsApi } from '../../../entity/users/api/settingsApi';
|
||||||
import type { UsersSettings } from '../../../entity/users/model/UsersSettings';
|
import type { UsersSettings } from '../../../entity/users/model/UsersSettings';
|
||||||
|
import { ClipboardHelper } from '../../../shared/lib/ClipboardHelper';
|
||||||
import { AuditLogsComponent } from './AuditLogsComponent';
|
import { AuditLogsComponent } from './AuditLogsComponent';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -247,7 +248,9 @@ export function SettingsComponent({ contentHeight }: Props) {
|
|||||||
size="small"
|
size="small"
|
||||||
className="ml-2 opacity-0 transition-opacity group-hover:opacity-100"
|
className="ml-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${getApplicationServer()}/api/v1/system/health`);
|
ClipboardHelper.copyToClipboard(
|
||||||
|
`${getApplicationServer()}/api/v1/system/health`,
|
||||||
|
);
|
||||||
message.success('Health-check endpoint copied to clipboard');
|
message.success('Health-check endpoint copied to clipboard');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
26
frontend/src/shared/lib/ClipboardHelper.ts
Normal file
26
frontend/src/shared/lib/ClipboardHelper.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export class ClipboardHelper {
|
||||||
|
static isClipboardApiAvailable(): boolean {
|
||||||
|
return !!(navigator.clipboard && window.isSecureContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async copyToClipboard(text: string): Promise<void> {
|
||||||
|
if (this.isClipboardApiAvailable()) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readFromClipboard(): Promise<string> {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
frontend/src/shared/ui/ClipboardPasteModalComponent.tsx
Normal file
52
frontend/src/shared/ui/ClipboardPasteModalComponent.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Button, Input, Modal } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onSubmit(text: string): void;
|
||||||
|
onCancel(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardPasteModalComponent({ open, onSubmit, onCancel }: Props) {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
onSubmit(trimmed);
|
||||||
|
setValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setValue('');
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Paste from clipboard"
|
||||||
|
open={open}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button onClick={handleCancel}>Cancel</Button>
|
||||||
|
<Button type="primary" disabled={!value.trim()} onClick={handleSubmit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Automatic clipboard access is not available. Please paste your content below.
|
||||||
|
</p>
|
||||||
|
<Input.TextArea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Paste your connection string here..."
|
||||||
|
rows={4}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { ClipboardPasteModalComponent } from './ClipboardPasteModalComponent';
|
||||||
export { CloudflareTurnstileWidget } from './CloudflareTurnstileWidget';
|
export { CloudflareTurnstileWidget } from './CloudflareTurnstileWidget';
|
||||||
export { ConfirmationComponent } from './ConfirmationComponent';
|
export { ConfirmationComponent } from './ConfirmationComponent';
|
||||||
export { StarButtonComponent } from './StarButtonComponent';
|
export { StarButtonComponent } from './StarButtonComponent';
|
||||||
|
|||||||
Reference in New Issue
Block a user