Sql Query Manager -> Move to Host Data
This commit is contained in:
parent
0f30c4ad7c
commit
84b9f65107
7 changed files with 449 additions and 47 deletions
|
|
@ -44,5 +44,10 @@ public interface ISqlObjectManagerAppService : IApplicationService
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lists .sql files currently available under DbMigrator Seeds/SqlData.
|
/// Lists .sql files currently available under DbMigrator Seeds/SqlData.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<SqlDataFileDto>> GetSqlDataFilesAsync();
|
Task<List<SqlDataFileDto>> GetSqlDataFilesAsync(string dataDirectoryName = "SqlData", string relativePath = "");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a SQL seed file between the selected data directory root and HostData.
|
||||||
|
/// </summary>
|
||||||
|
Task MoveSqlDataFileAsync(MoveSqlDataFileDto input);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,8 @@ namespace Sozsoft.SqlQueryManager.Application.Contracts;
|
||||||
public class SqlDataFileDto
|
public class SqlDataFileDto
|
||||||
{
|
{
|
||||||
public string FileName { get; set; } = string.Empty;
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string RelativePath { get; set; } = string.Empty;
|
||||||
|
public bool IsDirectory { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,10 @@ public class DeleteSqlDataFilesDto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> FileNames { get; set; } = new();
|
public List<string> FileNames { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class MoveSqlDataFileDto
|
||||||
|
{
|
||||||
|
public string DataDirectoryName { get; set; } = string.Empty;
|
||||||
|
public string SourceRelativePath { get; set; } = string.Empty;
|
||||||
|
public string TargetRelativePath { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -955,27 +955,48 @@ FROM (
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/app/sql-object-manager/sql-data-files")]
|
[HttpGet("api/app/sql-object-manager/sql-data-files")]
|
||||||
public Task<List<SqlDataFileDto>> GetSqlDataFilesAsync()
|
public Task<List<SqlDataFileDto>> GetSqlDataFilesAsync(
|
||||||
|
[FromQuery] string dataDirectoryName = "SqlData",
|
||||||
|
[FromQuery] string relativePath = "")
|
||||||
{
|
{
|
||||||
ValidateTenantAccess();
|
ValidateTenantAccess();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var outputPath = ResolveSqlDataOutputPath();
|
var rootPath = ResolveSqlDataOutputPath(dataDirectoryName);
|
||||||
|
var outputPath = ResolveSqlDataChildPath(rootPath, relativePath);
|
||||||
if (!Directory.Exists(outputPath))
|
if (!Directory.Exists(outputPath))
|
||||||
return Task.FromResult(new List<SqlDataFileDto>());
|
return Task.FromResult(new List<SqlDataFileDto>());
|
||||||
|
|
||||||
|
var directories = Directory.GetDirectories(outputPath, "*", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(d => string.Equals(Path.GetFileName(d), "HostData", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(d => new SqlDataFileDto
|
||||||
|
{
|
||||||
|
FileName = Path.GetFileName(d)!,
|
||||||
|
Name = Path.GetFileName(d)!,
|
||||||
|
RelativePath = BuildSqlDataRelativePath(relativePath, Path.GetFileName(d)!),
|
||||||
|
IsDirectory = true,
|
||||||
|
CreatedAt = Directory.GetCreationTime(d)
|
||||||
|
});
|
||||||
|
|
||||||
var files = Directory.GetFiles(outputPath, "*.sql", SearchOption.TopDirectoryOnly)
|
var files = Directory.GetFiles(outputPath, "*.sql", SearchOption.TopDirectoryOnly)
|
||||||
.Select(f => new SqlDataFileDto
|
.Select(f => new SqlDataFileDto
|
||||||
{
|
{
|
||||||
FileName = Path.GetFileName(f)!,
|
FileName = Path.GetFileName(f)!,
|
||||||
|
Name = Path.GetFileName(f)!,
|
||||||
|
RelativePath = BuildSqlDataRelativePath(relativePath, Path.GetFileName(f)!),
|
||||||
|
IsDirectory = false,
|
||||||
CreatedAt = File.GetCreationTime(f)
|
CreatedAt = File.GetCreationTime(f)
|
||||||
})
|
})
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.FileName))
|
.Where(x => !string.IsNullOrWhiteSpace(x.Name));
|
||||||
.OrderBy(x => x.FileName, StringComparer.OrdinalIgnoreCase)
|
|
||||||
|
var entries = directories
|
||||||
|
.Concat(files)
|
||||||
|
.OrderByDescending(x => x.IsDirectory)
|
||||||
|
.ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Task.FromResult(files);
|
return Task.FromResult(entries);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -984,24 +1005,121 @@ FROM (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/app/sql-object-manager/move-sql-data-file")]
|
||||||
|
public Task MoveSqlDataFileAsync(MoveSqlDataFileDto input)
|
||||||
|
{
|
||||||
|
ValidateTenantAccess();
|
||||||
|
|
||||||
|
if (input == null)
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Invalid move request.");
|
||||||
|
|
||||||
|
var rootPath = ResolveSqlDataOutputPath(input.DataDirectoryName);
|
||||||
|
var sourcePath = ResolveSqlDataChildPath(rootPath, input.SourceRelativePath);
|
||||||
|
var targetPath = ResolveSqlDataChildPath(rootPath, input.TargetRelativePath);
|
||||||
|
|
||||||
|
if (!File.Exists(sourcePath))
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Source file was not found.");
|
||||||
|
|
||||||
|
if (!string.Equals(Path.GetExtension(sourcePath), ".sql", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!string.Equals(Path.GetExtension(targetPath), ".sql", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Only .sql files can be moved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||||
|
|
||||||
|
if (File.Exists(targetPath))
|
||||||
|
throw new Volo.Abp.UserFriendlyException("A file with the same name already exists in the target folder.");
|
||||||
|
|
||||||
|
File.Move(sourcePath, targetPath);
|
||||||
|
_logger.LogInformation("SQL seed file moved from {SourcePath} to {TargetPath}", sourcePath, targetPath);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private string ResolveSqlDataOutputPath()
|
private string ResolveSqlDataOutputPath()
|
||||||
|
{
|
||||||
|
return ResolveSqlDataOutputPath("SqlData");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveSqlDataOutputPath(string dataDirectoryName)
|
||||||
{
|
{
|
||||||
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
|
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
|
||||||
|
var safeDirectoryName = NormalizeSqlDataDirectoryName(dataDirectoryName);
|
||||||
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
|
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
|
||||||
|
|
||||||
while (dir != null)
|
while (dir != null)
|
||||||
{
|
{
|
||||||
var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds");
|
var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds");
|
||||||
if (Directory.Exists(candidate))
|
if (Directory.Exists(candidate))
|
||||||
return Path.Combine(candidate, "SqlData");
|
return Path.Combine(candidate, safeDirectoryName);
|
||||||
|
|
||||||
candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds");
|
candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds");
|
||||||
if (Directory.Exists(candidate))
|
if (Directory.Exists(candidate))
|
||||||
return Path.Combine(candidate, "SqlData");
|
return Path.Combine(candidate, safeDirectoryName);
|
||||||
|
|
||||||
dir = dir.Parent;
|
dir = dir.Parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", "SqlData");
|
return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", safeDirectoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSqlDataDirectoryName(string dataDirectoryName)
|
||||||
|
{
|
||||||
|
return string.Equals(dataDirectoryName, "PostgresData", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "PostgresData"
|
||||||
|
: "SqlData";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSqlDataChildPath(string rootPath, string relativePath)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeSqlDataRelativePath(relativePath);
|
||||||
|
var fullPath = Path.GetFullPath(Path.Combine(rootPath, normalized));
|
||||||
|
var fullRoot = Path.GetFullPath(rootPath);
|
||||||
|
var fullRootWithSeparator = fullRoot.EndsWith(Path.DirectorySeparatorChar)
|
||||||
|
? fullRoot
|
||||||
|
: fullRoot + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
if (!string.Equals(fullPath, fullRoot, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!fullPath.StartsWith(fullRootWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Invalid path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSqlDataRelativePath(string relativePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(relativePath))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var normalized = relativePath.Replace('\\', '/').Trim('/');
|
||||||
|
var parts = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (parts.Any(p => p == "." || p == ".." || p.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0))
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Invalid path.");
|
||||||
|
|
||||||
|
var isRootFile = parts.Length == 1 && parts[0].EndsWith(".sql", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isHostDataFolder = parts.Length == 1 && string.Equals(parts[0], "HostData", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isHostDataFile = parts.Length == 2 &&
|
||||||
|
string.Equals(parts[0], "HostData", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
parts[1].EndsWith(".sql", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!isRootFile && !isHostDataFolder && !isHostDataFile)
|
||||||
|
throw new Volo.Abp.UserFriendlyException("Invalid path.");
|
||||||
|
|
||||||
|
return Path.Combine(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSqlDataRelativePath(string parentRelativePath, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(parentRelativePath))
|
||||||
|
return name;
|
||||||
|
|
||||||
|
return $"{parentRelativePath.Trim('/', '\\')}/{name}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17782,15 +17782,45 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
"key": "App.SqlQueryManager.MoveFiles",
|
||||||
|
"en": "Move to Host Folder",
|
||||||
|
"tr": "Host Klasörüne Taşı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.SqlQueryManager.MoveToHostData",
|
||||||
|
"en": "Move to Host Data",
|
||||||
|
"tr": "Host Klasörüne Taşı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.SqlQueryManager.MoveOut",
|
||||||
|
"en": "Move Out",
|
||||||
|
"tr": "Dışarı Taşı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.SqlQueryManager.NoIndexesDefined",
|
||||||
|
"en": "No indexes defined",
|
||||||
|
"tr": "Dizin tanımlanmamış"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
"key": "App.SqlQueryManager.NoIndexesDefined",
|
"key": "App.SqlQueryManager.NoIndexesDefined",
|
||||||
"en": "No indexes defined",
|
"en": "No indexes defined",
|
||||||
"tr": "Dizin tanımlanmamış"
|
"tr": "Dizin tanımlanmamış"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.SqlQueryManager.IndexKey",
|
"key": "App.Platform.OperationCompleted",
|
||||||
"en": "Index Key",
|
"en": "Operation completed successfully",
|
||||||
"tr": "Dizin Anahtarı"
|
"tr": "İşlem başarıyla tamamlandı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.SqlQueryManager.NoSqlDataFiles",
|
||||||
|
"en": "No SQL data files found",
|
||||||
|
"tr": "SQL veri dosyası bulunamadı"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,28 @@ export class SqlObjectManagerService {
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
||||||
getSqlDataFiles = (config?: Partial<Config>) =>
|
getSqlDataFiles = (dataDirectoryName = 'SqlData', relativePath = '', config?: Partial<Config>) =>
|
||||||
apiService.fetchData<{ fileName: string; createdAt: string }[], void>(
|
apiService.fetchData<
|
||||||
|
{ fileName: string; name: string; relativePath: string; isDirectory: boolean; createdAt: string }[],
|
||||||
|
void
|
||||||
|
>(
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/app/sql-object-manager/sql-data-files',
|
url: '/api/app/sql-object-manager/sql-data-files',
|
||||||
|
params: { dataDirectoryName, relativePath },
|
||||||
|
},
|
||||||
|
{ apiName: this.apiName, ...config },
|
||||||
|
)
|
||||||
|
|
||||||
|
moveSqlDataFile = (
|
||||||
|
input: { dataDirectoryName: string; sourceRelativePath: string; targetRelativePath: string },
|
||||||
|
config?: Partial<Config>,
|
||||||
|
) =>
|
||||||
|
apiService.fetchData<void, { dataDirectoryName: string; sourceRelativePath: string; targetRelativePath: string }>(
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/app/sql-object-manager/move-sql-data-file',
|
||||||
|
data: input,
|
||||||
},
|
},
|
||||||
{ apiName: this.apiName, ...config },
|
{ apiName: this.apiName, ...config },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { Button, Dialog, Notification, toast } from '@/components/ui'
|
import type { Dispatch, SetStateAction } from 'react'
|
||||||
|
import { Button, Checkbox, Dialog, Notification, toast } from '@/components/ui'
|
||||||
import Container from '@/components/shared/Container'
|
import Container from '@/components/shared/Container'
|
||||||
import ConfirmDialog from '@/components/shared/ConfirmDialog'
|
import ConfirmDialog from '@/components/shared/ConfirmDialog'
|
||||||
import { getDataSources } from '@/services/data-source.service'
|
import { getDataSources } from '@/services/data-source.service'
|
||||||
|
|
@ -7,7 +8,15 @@ import type { DataSourceDto } from '@/proxy/data-source'
|
||||||
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
import { DataSourceTypeEnum } from '@/proxy/form/models'
|
||||||
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
|
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { FaDatabase, FaPlay, FaFileAlt, FaCopy, FaExclamationTriangle } from 'react-icons/fa'
|
import {
|
||||||
|
FaDatabase,
|
||||||
|
FaPlay,
|
||||||
|
FaFileAlt,
|
||||||
|
FaCopy,
|
||||||
|
FaExclamationTriangle,
|
||||||
|
FaArrowLeft,
|
||||||
|
FaArrowRight,
|
||||||
|
} from 'react-icons/fa'
|
||||||
import { FaCheckCircle } from 'react-icons/fa'
|
import { FaCheckCircle } from 'react-icons/fa'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
|
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
|
||||||
|
|
@ -20,7 +29,7 @@ import { useStoreState } from '@/store/store'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { UiEvalService } from '@/services/UiEvalService'
|
import { UiEvalService } from '@/services/UiEvalService'
|
||||||
import { FcAcceptDatabase } from 'react-icons/fc'
|
import { FcAcceptDatabase } from 'react-icons/fc'
|
||||||
import { FaFolderOpen } from "react-icons/fa"
|
import { FaFolderOpen } from 'react-icons/fa'
|
||||||
|
|
||||||
interface SqlManagerState {
|
interface SqlManagerState {
|
||||||
dataSources: DataSourceDto[]
|
dataSources: DataSourceDto[]
|
||||||
|
|
@ -42,6 +51,14 @@ interface SqlCopyResultItem {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SqlDataExplorerEntry {
|
||||||
|
fileName: string
|
||||||
|
name: string
|
||||||
|
relativePath: string
|
||||||
|
isDirectory: boolean
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
const SqlQueryManager = () => {
|
const SqlQueryManager = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const editorRef = useRef<SqlEditorRef>(null)
|
const editorRef = useRef<SqlEditorRef>(null)
|
||||||
|
|
@ -82,7 +99,11 @@ const SqlQueryManager = () => {
|
||||||
const [sqlScriptForCopy, setSqlScriptForCopy] = useState('')
|
const [sqlScriptForCopy, setSqlScriptForCopy] = useState('')
|
||||||
const [showSqlDataFilesDialog, setShowSqlDataFilesDialog] = useState(false)
|
const [showSqlDataFilesDialog, setShowSqlDataFilesDialog] = useState(false)
|
||||||
const [isLoadingSqlDataFiles, setIsLoadingSqlDataFiles] = useState(false)
|
const [isLoadingSqlDataFiles, setIsLoadingSqlDataFiles] = useState(false)
|
||||||
const [sqlDataFiles, setSqlDataFiles] = useState<{ fileName: string; createdAt: string }[]>([])
|
const [isMovingSqlDataFile, setIsMovingSqlDataFile] = useState(false)
|
||||||
|
const [sqlDataRootFiles, setSqlDataRootFiles] = useState<SqlDataExplorerEntry[]>([])
|
||||||
|
const [sqlDataHostFiles, setSqlDataHostFiles] = useState<SqlDataExplorerEntry[]>([])
|
||||||
|
const [selectedRootSqlDataFiles, setSelectedRootSqlDataFiles] = useState<string[]>([])
|
||||||
|
const [selectedHostSqlDataFiles, setSelectedHostSqlDataFiles] = useState<string[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDataSources()
|
loadDataSources()
|
||||||
|
|
@ -150,6 +171,7 @@ const SqlQueryManager = () => {
|
||||||
(item) => item.code === state.selectedDataSource,
|
(item) => item.code === state.selectedDataSource,
|
||||||
)?.dataSourceType
|
)?.dataSourceType
|
||||||
const isPostgreSql = selectedDataSourceType === DataSourceTypeEnum.Postgresql
|
const isPostgreSql = selectedDataSourceType === DataSourceTypeEnum.Postgresql
|
||||||
|
const sqlDataDirectoryName = isPostgreSql ? 'PostgresData' : 'SqlData'
|
||||||
|
|
||||||
const buildTableScriptQuery = (schemaName: string, tableName: string) => {
|
const buildTableScriptQuery = (schemaName: string, tableName: string) => {
|
||||||
const fullName = getSafeFullName(schemaName, tableName)
|
const fullName = getSafeFullName(schemaName, tableName)
|
||||||
|
|
@ -990,15 +1012,24 @@ GO`,
|
||||||
const copyErrorCount = copyResults.filter((x) => x.status === 'error').length
|
const copyErrorCount = copyResults.filter((x) => x.status === 'error').length
|
||||||
const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length
|
const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length
|
||||||
|
|
||||||
const handleOpenSqlDataFilesDialog = async () => {
|
const loadSqlDataFiles = async () => {
|
||||||
setShowSqlDataFilesDialog(true)
|
|
||||||
setIsLoadingSqlDataFiles(true)
|
setIsLoadingSqlDataFiles(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sqlObjectManagerService.getSqlDataFiles()
|
const [rootResponse, hostResponse] = await Promise.all([
|
||||||
setSqlDataFiles(response.data || [])
|
sqlObjectManagerService.getSqlDataFiles(sqlDataDirectoryName, ''),
|
||||||
|
sqlObjectManagerService.getSqlDataFiles(sqlDataDirectoryName, 'HostData'),
|
||||||
|
])
|
||||||
|
|
||||||
|
setSqlDataRootFiles(normalizeSqlDataEntries(rootResponse.data || [], ''))
|
||||||
|
setSqlDataHostFiles(normalizeSqlDataEntries(hostResponse.data || [], 'HostData'))
|
||||||
|
setSelectedRootSqlDataFiles([])
|
||||||
|
setSelectedHostSqlDataFiles([])
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSqlDataFiles([])
|
setSqlDataRootFiles([])
|
||||||
|
setSqlDataHostFiles([])
|
||||||
|
setSelectedRootSqlDataFiles([])
|
||||||
|
setSelectedHostSqlDataFiles([])
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||||
{error.response?.data?.error?.message ||
|
{error.response?.data?.error?.message ||
|
||||||
|
|
@ -1012,6 +1043,148 @@ GO`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenSqlDataFilesDialog = async () => {
|
||||||
|
setShowSqlDataFilesDialog(true)
|
||||||
|
await loadSqlDataFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeSqlDataEntries = (
|
||||||
|
files: SqlDataExplorerEntry[],
|
||||||
|
parentRelativePath: '' | 'HostData',
|
||||||
|
) =>
|
||||||
|
files
|
||||||
|
.filter((file) => !file.isDirectory)
|
||||||
|
.map((file) => {
|
||||||
|
const fileName = file.name || file.fileName || file.relativePath?.split('/').pop() || ''
|
||||||
|
const relativePath =
|
||||||
|
file.relativePath || (parentRelativePath ? `${parentRelativePath}/${fileName}` : fileName)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
fileName,
|
||||||
|
name: fileName,
|
||||||
|
relativePath,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((file) => Boolean(file.name && file.relativePath))
|
||||||
|
|
||||||
|
const toggleSqlDataFileSelection = (
|
||||||
|
setSelectedFiles: Dispatch<SetStateAction<string[]>>,
|
||||||
|
relativePath: string,
|
||||||
|
) => {
|
||||||
|
setSelectedFiles((current) =>
|
||||||
|
current.includes(relativePath)
|
||||||
|
? current.filter((item) => item !== relativePath)
|
||||||
|
: [...current, relativePath],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveSqlDataFiles = async (direction: 'toHostData' | 'toRoot') => {
|
||||||
|
const selectedRelativePaths =
|
||||||
|
direction === 'toHostData' ? selectedRootSqlDataFiles : selectedHostSqlDataFiles
|
||||||
|
|
||||||
|
if (selectedRelativePaths.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsMovingSqlDataFile(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const sourceRelativePath of selectedRelativePaths) {
|
||||||
|
const name = sourceRelativePath.split('/').pop() || sourceRelativePath
|
||||||
|
await sqlObjectManagerService.moveSqlDataFile({
|
||||||
|
dataDirectoryName: sqlDataDirectoryName,
|
||||||
|
sourceRelativePath,
|
||||||
|
targetRelativePath: direction === 'toHostData' ? `HostData/${name}` : name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadSqlDataFiles()
|
||||||
|
toast.push(
|
||||||
|
<Notification type="success" title={translate('::App.Platform.Success')}>
|
||||||
|
{translate('::App.Platform.OperationCompleted') || 'Dosya tasindi.'}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-center' },
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="danger" title={translate('::App.Platform.Error')}>
|
||||||
|
{error.response?.data?.error?.message || 'Dosya tasinamadi.'}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-center' },
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setIsMovingSqlDataFile(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSqlDataPane = (
|
||||||
|
title: string,
|
||||||
|
files: SqlDataExplorerEntry[],
|
||||||
|
selectedFiles: string[],
|
||||||
|
setSelectedFiles: Dispatch<SetStateAction<string[]>>,
|
||||||
|
) => (
|
||||||
|
<div className="flex min-h-[260px] flex-1 flex-col overflow-hidden rounded border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex shrink-0 items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h6 className="text-sm font-semibold text-gray-800 dark:text-gray-100">{title}</h6>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{selectedFiles.length}/{files.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={files.length > 0 && selectedFiles.length === files.length}
|
||||||
|
disabled={files.length === 0 || isMovingSqlDataFile}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSelectedFiles(event ? files.map((file) => file.relativePath) : [])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{translate('::FileManager.SelectAll')}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{files.length === 0 ? (
|
||||||
|
<p className="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{translate('::App.SqlQueryManager.NoSqlDataFiles') || 'Dosya bulunamadi.'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{files.map((file) => {
|
||||||
|
const selected = selectedFiles.includes(file.relativePath)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={file.relativePath || file.fileName}>
|
||||||
|
<label
|
||||||
|
className={`flex cursor-pointer items-center gap-3 px-3 py-2 text-xs ${
|
||||||
|
selected
|
||||||
|
? 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-200'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 shrink-0"
|
||||||
|
checked={selected}
|
||||||
|
disabled={isMovingSqlDataFile}
|
||||||
|
onChange={() =>
|
||||||
|
toggleSqlDataFileSelection(setSelectedFiles, file.relativePath)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FaFileAlt className="shrink-0 text-gray-400" />
|
||||||
|
<span className="min-w-0 flex-1 truncate">{file.name || file.fileName}</span>
|
||||||
|
<span className="hidden shrink-0 text-xs text-gray-400 dark:text-gray-500 sm:inline">
|
||||||
|
{new Date(file.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
|
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
|
||||||
<Helmet
|
<Helmet
|
||||||
|
|
@ -1056,9 +1229,9 @@ GO`,
|
||||||
icon={<FaFolderOpen />}
|
icon={<FaFolderOpen />}
|
||||||
onClick={handleOpenSqlDataFilesDialog}
|
onClick={handleOpenSqlDataFilesDialog}
|
||||||
className="shadow-sm px-2 py-1"
|
className="shadow-sm px-2 py-1"
|
||||||
title={translate('::App.SqlQueryManager.SqlDataFiles') || 'Show SqlData files'}
|
title={translate('::App.SqlQueryManager.MoveFiles')}
|
||||||
>
|
>
|
||||||
{translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'}
|
{translate('::App.SqlQueryManager.MoveFiles')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1219,7 +1392,8 @@ GO`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
{translate('::App.DbMigrate.ConfirmMessage') || 'Are you sure you want to start the database migration process?'}
|
{translate('::App.DbMigrate.ConfirmMessage') ||
|
||||||
|
'Are you sure you want to start the database migration process?'}
|
||||||
</p>
|
</p>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
|
@ -1227,36 +1401,79 @@ GO`,
|
||||||
isOpen={showSqlDataFilesDialog}
|
isOpen={showSqlDataFilesDialog}
|
||||||
onClose={() => setShowSqlDataFilesDialog(false)}
|
onClose={() => setShowSqlDataFilesDialog(false)}
|
||||||
onRequestClose={() => setShowSqlDataFilesDialog(false)}
|
onRequestClose={() => setShowSqlDataFilesDialog(false)}
|
||||||
|
width={1050}
|
||||||
contentClassName="max-h-[90vh] overflow-hidden"
|
contentClassName="max-h-[90vh] overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="flex max-h-[72vh] min-h-[320px] flex-col">
|
<Dialog.Body className="flex max-h-[90vh] min-h-[420px] flex-col gap-2">
|
||||||
<h5 className="mb-4 shrink-0">
|
<div className="mb-4 shrink-0">
|
||||||
{translate('::App.SqlQueryManager.SqlDataFiles') || 'SqlData Files'}
|
<div>
|
||||||
</h5>
|
<h5>{translate('::App.SqlQueryManager.MoveFiles')}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoadingSqlDataFiles ? (
|
{isLoadingSqlDataFiles ? (
|
||||||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||||||
{translate('::App.Platform.Loading') || 'Loading...'}
|
{translate('::App.Platform.Loading') || 'Loading...'}
|
||||||
</p>
|
</p>
|
||||||
) : sqlDataFiles.length === 0 ? (
|
|
||||||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
|
||||||
{translate('::App.SqlQueryManager.NoSqlDataFiles') || 'SqlData klasorunde .sql dosyasi bulunamadi.'}
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-4 h-[70vh] min-h-[220px] overflow-y-auto rounded border border-gray-200 dark:border-gray-700">
|
<div className="flex min-h-0 flex-1 flex-col gap-3 lg:flex-row">
|
||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
{renderSqlDataPane(
|
||||||
{sqlDataFiles.map((file) => (
|
sqlDataDirectoryName,
|
||||||
<li key={file.fileName} className="flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200">
|
sqlDataRootFiles,
|
||||||
<span>{file.fileName}</span>
|
selectedRootSqlDataFiles,
|
||||||
<span className="ml-4 shrink-0 text-xs text-gray-400 dark:text-gray-500">
|
setSelectedRootSqlDataFiles,
|
||||||
{new Date(file.createdAt).toLocaleString()}
|
)}
|
||||||
</span>
|
|
||||||
</li>
|
<div className="flex shrink-0 flex-row items-center justify-center gap-2 lg:w-24 lg:flex-col">
|
||||||
))}
|
<Button
|
||||||
</ul>
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
icon={<FaArrowRight />}
|
||||||
|
onClick={() => handleMoveSqlDataFiles('toHostData')}
|
||||||
|
loading={isMovingSqlDataFile}
|
||||||
|
disabled={isMovingSqlDataFile || selectedRootSqlDataFiles.length === 0}
|
||||||
|
title={
|
||||||
|
translate('::App.SqlQueryManager.MoveToHostData') || 'HostData klasorune tasi'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
icon={<FaArrowLeft />}
|
||||||
|
onClick={() => handleMoveSqlDataFiles('toRoot')}
|
||||||
|
loading={isMovingSqlDataFile}
|
||||||
|
disabled={isMovingSqlDataFile || selectedHostSqlDataFiles.length === 0}
|
||||||
|
title={translate('::App.SqlQueryManager.MoveOut') || 'Disari tasi'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderSqlDataPane(
|
||||||
|
'HostData',
|
||||||
|
sqlDataHostFiles,
|
||||||
|
selectedHostSqlDataFiles,
|
||||||
|
setSelectedHostSqlDataFiles,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Dialog.Body>
|
||||||
|
|
||||||
|
<Dialog.Footer className="flex justify-end gap-2 pt-3 mt-1">
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
onClick={loadSqlDataFiles}
|
||||||
|
loading={isLoadingSqlDataFiles}
|
||||||
|
disabled={isLoadingSqlDataFiles || isMovingSqlDataFile}
|
||||||
|
>
|
||||||
|
{translate('::App.Platform.Refresh') || 'Yenile'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => setShowSqlDataFilesDialog(false)}
|
||||||
|
disabled={isMovingSqlDataFile}
|
||||||
|
>
|
||||||
|
{translate('::Close') || translate('::Cancel') || 'Kapat'}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Template Confirmation Dialog */}
|
{/* Template Confirmation Dialog */}
|
||||||
|
|
@ -1273,7 +1490,12 @@ GO`,
|
||||||
<Button variant="plain" onClick={handleCancelTemplateReplace}>
|
<Button variant="plain" onClick={handleCancelTemplateReplace}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="solid" onClick={handleConfirmTemplateReplace} icon={<FaExclamationTriangle />} color="red-600">
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
onClick={handleConfirmTemplateReplace}
|
||||||
|
icon={<FaExclamationTriangle />}
|
||||||
|
color="red-600"
|
||||||
|
>
|
||||||
{translate('::App.Platform.Replace')}
|
{translate('::App.Platform.Replace')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue