mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-09 08:25:57 +00:00
add different clients support
This commit is contained in:
committed by
Guy Ben-Aharon
parent
1fbf7cc677
commit
eee9832fe1
31
package-lock.json
generated
31
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
@@ -2192,6 +2193,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ export interface CodeSnippetProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
codeProps?: CodeBlockProps;
|
codeProps?: CodeBlockProps;
|
||||||
code: string;
|
code: string;
|
||||||
|
language?: 'sql' | 'bash';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
|
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
|
||||||
className,
|
className,
|
||||||
codeProps,
|
codeProps,
|
||||||
code,
|
code,
|
||||||
|
language = 'sql',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-1', className)}>
|
<div className={cn('flex flex-1', className)}>
|
||||||
<CopyBlock
|
<CopyBlock
|
||||||
language="sql"
|
language={language}
|
||||||
text={code}
|
text={code}
|
||||||
theme={atomOneDark}
|
theme={atomOneDark}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
|
|||||||
53
src/components/tabs/tabs.tsx
Normal file
53
src/components/tabs/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -15,9 +15,9 @@ import { generateId } from '@/lib/utils';
|
|||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
import { CreateDiagramDialogSelectDatabase } from './create-diagram-dialog-select-database/create-diagram-dialog-select-database';
|
import { SelectDatabaseStep } from './select-database-step/select-database-step';
|
||||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||||
import { CreateDiagramDialogImportDatabase } from './create-diagram-dialog-import-database/create-diagram-dialog-import-database';
|
import { ImportDatabaseStep } from './import-database-step/import-database-step';
|
||||||
|
|
||||||
const errorScriptOutputMessage =
|
const errorScriptOutputMessage =
|
||||||
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
|
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
|
||||||
@@ -142,11 +142,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex w-[90vw] flex-col overflow-visible xl:min-w-[45vw]"
|
className="flex max-h-[90vh] w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
|
||||||
showClose={hasExistingDiagram}
|
showClose={hasExistingDiagram}
|
||||||
>
|
>
|
||||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||||
<CreateDiagramDialogSelectDatabase
|
<SelectDatabaseStep
|
||||||
createNewDiagram={createNewDiagram}
|
createNewDiagram={createNewDiagram}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
hasExistingDiagram={hasExistingDiagram}
|
hasExistingDiagram={hasExistingDiagram}
|
||||||
@@ -154,7 +154,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
setStep={setStep}
|
setStep={setStep}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CreateDiagramDialogImportDatabase
|
<ImportDatabaseStep
|
||||||
createNewDiagram={createNewDiagram}
|
createNewDiagram={createNewDiagram}
|
||||||
databaseEdition={databaseEdition}
|
databaseEdition={databaseEdition}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import {
|
import {
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -26,8 +26,14 @@ import {
|
|||||||
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
||||||
import { SSMSInfo } from './ssms-info/ssms-info';
|
import { SSMSInfo } from './ssms-info/ssms-info';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
|
||||||
|
import {
|
||||||
|
DatabaseClient,
|
||||||
|
databaseClientToLabelMap,
|
||||||
|
databaseTypeToClientsMap,
|
||||||
|
} from '@/lib/domain/database-clients';
|
||||||
|
|
||||||
export interface CreateDiagramDialogImportDatabaseProps {
|
export interface ImportDatabaseStepProps {
|
||||||
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
||||||
createNewDiagram: () => void;
|
createNewDiagram: () => void;
|
||||||
scriptResult: string;
|
scriptResult: string;
|
||||||
@@ -40,9 +46,7 @@ export interface CreateDiagramDialogImportDatabaseProps {
|
|||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateDiagramDialogImportDatabase: React.FC<
|
export const ImportDatabaseStep: React.FC<ImportDatabaseStepProps> = ({
|
||||||
CreateDiagramDialogImportDatabaseProps
|
|
||||||
> = ({
|
|
||||||
setScriptResult,
|
setScriptResult,
|
||||||
setStep,
|
setStep,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
@@ -52,6 +56,10 @@ export const CreateDiagramDialogImportDatabase: React.FC<
|
|||||||
setDatabaseEdition,
|
setDatabaseEdition,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}) => {
|
}) => {
|
||||||
|
const databaseClients = databaseTypeToClientsMap[databaseType];
|
||||||
|
const [databaseClient, setDatabaseClient] = useState<
|
||||||
|
DatabaseClient | undefined
|
||||||
|
>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@@ -158,12 +166,56 @@ export const CreateDiagramDialogImportDatabase: React.FC<
|
|||||||
<SSMSInfo />
|
<SSMSInfo />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CodeSnippet
|
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
||||||
className="max-h-40 w-full"
|
<Tabs
|
||||||
code={importMetadataScripts[databaseType]({
|
value={
|
||||||
databaseEdition,
|
!databaseClient ? 'dbclient' : databaseClient
|
||||||
})}
|
}
|
||||||
/>
|
onValueChange={(value) => {
|
||||||
|
setDatabaseClient(
|
||||||
|
value === 'dbclient'
|
||||||
|
? undefined
|
||||||
|
: (value as DatabaseClient)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
|
||||||
|
<TabsTrigger
|
||||||
|
value="dbclient"
|
||||||
|
className="h-6 w-20"
|
||||||
|
>
|
||||||
|
DB Client
|
||||||
|
</TabsTrigger>
|
||||||
|
|
||||||
|
{databaseClients?.map((client) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={client}
|
||||||
|
value={client}
|
||||||
|
className="h-6 !w-20"
|
||||||
|
>
|
||||||
|
{databaseClientToLabelMap[client]}
|
||||||
|
</TabsTrigger>
|
||||||
|
)) ?? []}
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
<CodeSnippet
|
||||||
|
className="max-h-40 w-full"
|
||||||
|
code={importMetadataScripts[databaseType]({
|
||||||
|
databaseEdition,
|
||||||
|
databaseClient,
|
||||||
|
})}
|
||||||
|
language={databaseClient ? 'bash' : 'sql'}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
) : (
|
||||||
|
<CodeSnippet
|
||||||
|
className="max-h-40 w-full"
|
||||||
|
code={importMetadataScripts[databaseType]({
|
||||||
|
databaseEdition,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-48 flex-col gap-1">
|
<div className="flex h-48 flex-col gap-1">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -192,6 +244,8 @@ export const CreateDiagramDialogImportDatabase: React.FC<
|
|||||||
handleInputChange,
|
handleInputChange,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
setDatabaseEdition,
|
setDatabaseEdition,
|
||||||
|
databaseClients,
|
||||||
|
databaseClient,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import { LayoutGrid } from 'lucide-react';
|
|||||||
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
import { CreateDiagramDialogStep } from '../create-diagram-dialog-step';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface CreateDiagramDialogSelectDatabaseProps {
|
export interface SelectDatabaseStepProps {
|
||||||
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
setStep: React.Dispatch<React.SetStateAction<CreateDiagramDialogStep>>;
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
||||||
@@ -24,9 +24,7 @@ export interface CreateDiagramDialogSelectDatabaseProps {
|
|||||||
createNewDiagram: () => void;
|
createNewDiagram: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateDiagramDialogSelectDatabase: React.FC<
|
export const SelectDatabaseStep: React.FC<SelectDatabaseStepProps> = ({
|
||||||
CreateDiagramDialogSelectDatabaseProps
|
|
||||||
> = ({
|
|
||||||
setStep,
|
setStep,
|
||||||
databaseType,
|
databaseType,
|
||||||
setDatabaseType,
|
setDatabaseType,
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DatabaseClient } from '@/lib/domain/database-clients';
|
||||||
import {
|
import {
|
||||||
DatabaseEdition,
|
DatabaseEdition,
|
||||||
databaseEditionToLabelMap,
|
databaseEditionToLabelMap,
|
||||||
@@ -6,6 +7,7 @@ import {
|
|||||||
export const getPostgresQuery = (
|
export const getPostgresQuery = (
|
||||||
options: {
|
options: {
|
||||||
databaseEdition?: DatabaseEdition;
|
databaseEdition?: DatabaseEdition;
|
||||||
|
databaseClient?: DatabaseClient;
|
||||||
} = {}
|
} = {}
|
||||||
): string => {
|
): string => {
|
||||||
const databaseEdition: DatabaseEdition | undefined =
|
const databaseEdition: DatabaseEdition | undefined =
|
||||||
@@ -227,5 +229,11 @@ SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
|
|||||||
FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views;
|
FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
||||||
|
return `psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
||||||
|
${query}
|
||||||
|
" -t -A | pbcopy; LG='\\033[0;32m'; NC='\\033[0m'; echo "You got the resultset ($(pbpaste | wc -c | xargs) characters) in Copy/Paste. \${LG}Go back & paste in ChartDB :)\${NC}";`;
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { sqliteQuery } from './sqlite-script';
|
|||||||
import { sqlServerQuery } from './sqlserver-script';
|
import { sqlServerQuery } from './sqlserver-script';
|
||||||
import { mariaDBQuery } from './maria-script';
|
import { mariaDBQuery } from './maria-script';
|
||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
|
import { DatabaseClient } from '@/lib/domain/database-clients';
|
||||||
|
|
||||||
export const importMetadataScripts: Record<
|
export const importMetadataScripts: Record<
|
||||||
DatabaseType,
|
DatabaseType,
|
||||||
(options?: { databaseEdition?: DatabaseEdition }) => string
|
(options?: {
|
||||||
|
databaseEdition?: DatabaseEdition;
|
||||||
|
databaseClient?: DatabaseClient;
|
||||||
|
}) => string
|
||||||
> = {
|
> = {
|
||||||
[DatabaseType.GENERIC]: () => '',
|
[DatabaseType.GENERIC]: () => '',
|
||||||
[DatabaseType.POSTGRESQL]: getPostgresQuery,
|
[DatabaseType.POSTGRESQL]: getPostgresQuery,
|
||||||
|
|||||||
21
src/lib/domain/database-clients.ts
Normal file
21
src/lib/domain/database-clients.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { DatabaseType } from './database-type';
|
||||||
|
|
||||||
|
export enum DatabaseClient {
|
||||||
|
// PostgreSQL
|
||||||
|
POSTGRESQL_PSQL = 'psql',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const databaseClientToLabelMap: Record<DatabaseClient, string> = {
|
||||||
|
// PostgreSQL
|
||||||
|
[DatabaseClient.POSTGRESQL_PSQL]: 'PSQL',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
|
||||||
|
{
|
||||||
|
[DatabaseType.POSTGRESQL]: [DatabaseClient.POSTGRESQL_PSQL],
|
||||||
|
[DatabaseType.MYSQL]: [],
|
||||||
|
[DatabaseType.SQLITE]: [],
|
||||||
|
[DatabaseType.GENERIC]: [],
|
||||||
|
[DatabaseType.SQL_SERVER]: [],
|
||||||
|
[DatabaseType.MARIADB]: [],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user