Sql Query Manager düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2025-12-05 16:45:45 +03:00
parent 3d419a6e7c
commit b9331e66b4
14 changed files with 3054 additions and 4 deletions

View file

@ -619,6 +619,12 @@
"en": "Forum Management",
"tr": "Forum Yönetimi"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager",
"en": "SQL Query Manager",
"tr": "SQL Sorgu Yöneticisi"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit",
@ -10314,6 +10320,552 @@
"key": "ListForms.SchedulerOptions.Agenda",
"tr": "Ajanda",
"en": "Agenda"
},
{
"resourceName": "Platform",
"key": "App.Platform.Error",
"tr": "Hata",
"en": "Error"
},
{
"resourceName": "Platform",
"key": "App.Platform.Edit",
"tr": "Düzenle",
"en": "Edit"
},
{
"resourceName": "Platform",
"key": "App.Platform.Delete",
"tr": "Sil",
"en": "Delete"
},
{
"resourceName": "Platform",
"key": "App.Platform.Warning",
"tr": "Uyarı",
"en": "Warning"
},
{
"resourceName": "Platform",
"key": "App.Platform.Success",
"tr": "Başarılı",
"en": "Success"
},
{
"resourceName": "Platform",
"key": "App.Platform.Execute",
"tr": "Çalıştır",
"en": "Execute"
},
{
"resourceName": "Platform",
"key": "App.Platform.Save",
"tr": "Kaydet",
"en": "Save"
},
{
"resourceName": "Platform",
"key": "App.Platform.Deploy",
"tr": "Dağıt",
"en": "Deploy"
},
{
"resourceName": "Platform",
"key": "App.Platform.Refresh",
"tr": "Tazele",
"en": "Refresh"
},
{
"resourceName": "Platform",
"key": "App.Platform.Replace",
"tr": "Değiştir",
"en": "Replace"
},
{
"resourceName": "Platform",
"key": "App.Platform.Cancel",
"tr": "İptal",
"en": "Cancel"
},
{
"resourceName": "Platform",
"key": "App.Platform.Loading",
"tr": "Yükleniyor...",
"en": "Loading..."
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedtoloadDatasources",
"tr": "Veri kaynakları yüklenemedi",
"en": "Failed to load data sources"
},
{
"resourceName": "Platform",
"key": "App.Platform.PleaseSelectDataSource",
"tr": "Lütfen bir veri kaynağı seçin",
"en": "Please select data source"
},
{
"resourceName": "Platform",
"key": "App.Platform.UnsavedChangesConfirmation",
"tr": "Kaydedilmemiş değişiklikleriniz var. Devam etmek istediğinizden emin misiniz?",
"en": "You have unsaved changes. Are you sure you want to continue?"
},
{
"resourceName": "Platform",
"key": "App.Platform.PleaseEnterQuery",
"tr": "Lütfen sorgu girin",
"en": "Please enter query"
},
{
"resourceName": "Platform",
"key": "App.Platform.QueryExecutedSuccessfully",
"tr": "Sorgu başarıyla çalıştırıldı",
"en": "Query executed successfully"
},
{
"resourceName": "Platform",
"key": "App.Platform.PleaseEnterContentToSave",
"tr": "Lütfen kaydetmek için içerik girin",
"en": "Please enter content to save"
},
{
"resourceName": "Platform",
"key": "App.Platform.ObjectUpdatedSuccessfully",
"tr": "Nesne başarıyla güncellendi",
"en": "Object updated successfully"
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedToUpdateObject",
"tr": "Nesne güncellenemedi",
"en": "Failed to update object"
},
{
"resourceName": "Platform",
"key": "App.Platform.QuerySavedSuccessfully",
"tr": "Sorgu başarıyla kaydedildi",
"en": "Query saved successfully"
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedToSaveQuery",
"tr": "Sorgu kaydedilemedi",
"en": "Failed to save query"
},
{
"resourceName": "Platform",
"key": "App.Platform.PleaseSelectAnObjectToDeploy",
"tr": "Lütfen dağıtmak için bir nesne seçin",
"en": "Please select an object to deploy"
},
{
"resourceName": "Platform",
"key": "App.Platform.ThisObjectTypeCannotBeDeployed",
"tr": "Bu nesne türü dağıtılamaz",
"en": "This object type cannot be deployed"
},
{
"resourceName": "Platform",
"key": "App.Platform.ObjectDeployedSuccessfully",
"tr": "Bu nesne türü başarıyla dağıtıldı",
"en": "Object deployed successfully"
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedToDeployObject",
"tr": "Bu nesne türü dağıtılamadı",
"en": "Failed to deploy object"
},
{
"resourceName": "Platform",
"key": "App.Platform.SaveAsNewQuery",
"tr": "Yeni Sorgu Olarak Kaydet",
"en": "Save as New Query"
},
{
"resourceName": "Platform",
"key": "App.Platform.ObjectDeletedSuccessfully",
"tr": "Nesne başarıyla silindi",
"en": "Object deleted successfully"
},
{
"resourceName": "Platform",
"key": "App.Platform.SelectAnObjectToViewProperties",
"tr": "Özellikleri görüntülemek için bir nesne seçin",
"en": "Select an object to view properties"
},
{
"resourceName": "Platform",
"key": "App.Platform.Query",
"tr": "Sorgu",
"en": "Query"
},
{
"resourceName": "Platform",
"key": "App.Platform.StoredProcedure",
"tr": "Saklı Yordam",
"en": "Stored Procedure"
},
{
"resourceName": "Platform",
"key": "App.Platform.View",
"tr": "View",
"en": "View"
},
{
"resourceName": "Platform",
"key": "App.Platform.Function",
"tr": "Fonksiyon",
"en": "Function"
},
{
"resourceName": "Platform",
"key": "App.Platform.Object",
"tr": "Nesne",
"en": "Object"
},
{
"resourceName": "Platform",
"key": "App.Platform.Draft",
"tr": "Taslak",
"en": "Draft"
},
{
"resourceName": "Platform",
"key": "App.Platform.Active",
"tr": "Aktif",
"en": "Active"
},
{
"resourceName": "Platform",
"key": "App.Platform.Archived",
"tr": "Arşivlenmiş",
"en": "Archived"
},
{
"resourceName": "Platform",
"key": "App.Platform.Unknown",
"tr": "Bilinmiyor",
"en": "Unknown"
},
{
"resourceName": "Platform",
"key": "App.Platform.ObjectType",
"tr": "Nesne Tipi",
"en": "Object Type"
},
{
"resourceName": "Platform",
"key": "App.Platform.ID",
"tr": "ID",
"en": "ID"
},
{
"resourceName": "Platform",
"key": "App.Platform.Created",
"tr": "Oluşturulma",
"en": "Created"
},
{
"resourceName": "Platform",
"key": "App.Platform.Modified",
"tr": "Güncellenme",
"en": "Modified"
},
{
"resourceName": "Platform",
"key": "App.Platform.Code",
"tr": "Kod",
"en": "Code"
},
{
"resourceName": "Platform",
"key": "App.Platform.Name",
"tr": "Ad",
"en": "Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.Description",
"tr": "Açıklama",
"en": "Description"
},
{
"resourceName": "Platform",
"key": "App.Platform.DataSource",
"tr": "Veri Kaynağı",
"en": "Data Source"
},
{
"resourceName": "Platform",
"key": "App.Platform.Status",
"tr": "Durum",
"en": "Status"
},
{
"resourceName": "Platform",
"key": "App.Platform.Category",
"tr": "Kategori",
"en": "Category"
},
{
"resourceName": "Platform",
"key": "App.Platform.Tags",
"tr": "Etiketler",
"en": "Tags"
},
{
"resourceName": "Platform",
"key": "App.Platform.ModifiesData",
"tr": "Veri Değiştirir",
"en": "Modifies Data"
},
{
"resourceName": "Platform",
"key": "App.Platform.Yes",
"tr": "Evet",
"en": "Yes"
},
{
"resourceName": "Platform",
"key": "App.Platform.No",
"tr": "Hayır",
"en": "No"
},
{
"resourceName": "Platform",
"key": "App.Platform.ExecutionCount",
"tr": "Çalıştırma Sayısı",
"en": "Execution Count"
},
{
"resourceName": "Platform",
"key": "App.Platform.LastExecuted",
"tr": "Son Çalıştırma",
"en": "Last Executed"
},
{
"resourceName": "Platform",
"key": "App.Platform.ProcedureName",
"tr": "Prosedür Adı",
"en": "Procedure Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.Schema",
"tr": "Şema",
"en": "Schema"
},
{
"resourceName": "Platform",
"key": "App.Platform.DisplayName",
"tr": "Görünen Ad",
"en": "Display Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.Deployed",
"tr": "Dağıtıldı",
"en": "Deployed"
},
{
"resourceName": "Platform",
"key": "App.Platform.LastDeployed",
"tr": "Son Dağıtım",
"en": "Last Deployed"
},
{
"resourceName": "Platform",
"key": "App.Platform.ViewName",
"tr": "View Adı",
"en": "View Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.SchemaBinding",
"tr": "Schema Binding",
"en": "Schema Binding"
},
{
"resourceName": "Platform",
"key": "App.Platform.FunctionName",
"tr": "Fonksiyon Adı",
"en": "Function Name"
},
{
"resourceName": "Platform",
"key": "App.Platform.FunctionType",
"tr": "Fonksiyon Tipi",
"en": "Function Type"
},
{
"resourceName": "Platform",
"key": "App.Platform.ReturnType",
"tr": "Dönüş Tipi",
"en": "Return Type"
},
{
"resourceName": "Platform",
"key": "App.Platform.ScalarFunction",
"tr": "Scalar Fonksiyon",
"en": "Scalar Function"
},
{
"resourceName": "Platform",
"key": "App.Platform.TableValuedFunction",
"tr": "Tablo Döndüren Fonksiyon",
"en": "Table-Valued Function"
},
{
"resourceName": "Platform",
"key": "App.Platform.InlineTableValuedFunction",
"tr": "Inline Tablo Döndüren Fonksiyon",
"en": "Inline Table-Valued Function"
},
{
"resourceName": "Platform",
"key": "App.Platform.Properties",
"tr": "Özellikler",
"en": "Properties"
},
{
"resourceName": "Platform",
"key": "App.Platform.Rows",
"tr": "Satır",
"en": "Rows"
},
{
"resourceName": "Platform",
"key": "App.Platform.Time",
"tr": "Süre",
"en": "Time"
},
{
"resourceName": "Platform",
"key": "App.Platform.Search",
"tr": "Ara...",
"en": "Search..."
},
{
"resourceName": "Platform",
"key": "App.Platform.NoResults",
"tr": "Sonuç bulunamadı",
"en": "No results"
},
{
"resourceName": "Platform",
"key": "App.Platform.RowCount",
"tr": "{{count}} satır etkilendi",
"en": "{{count}} row(s) affected"
},
{
"resourceName": "Platform",
"key": "App.Platform.NoResultsReturned",
"tr": "Sorgu başarıyla çalıştı ancak sonuç döndürmedi.",
"en": "The query executed successfully but returned no results."
},
{
"resourceName": "Platform",
"key": "App.Platform.Queries",
"tr": "Sorgular",
"en": "Queries"
},
{
"resourceName": "Platform",
"key": "App.Platform.StoredProcedures",
"tr": "Saklı Yordamlar",
"en": "Stored Procedures"
},
{
"resourceName": "Platform",
"key": "App.Platform.Views",
"tr": "Görünümler",
"en": "Views"
},
{
"resourceName": "Platform",
"key": "App.Platform.Functions",
"tr": "Fonksiyonlar",
"en": "Functions"
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedToLoadObjects",
"tr": "Nesneler yüklenemedi",
"en": "Failed to load objects"
},
{
"resourceName": "Platform",
"key": "App.Platform.ObjectExplorer",
"tr": "Nesne Gezgini",
"en": "Object Explorer"
},
{
"resourceName": "Platform",
"key": "App.Platform.QueryEditor",
"tr": "Sorgu Editörü",
"en": "Query Editor"
},
{
"resourceName": "Platform",
"key": "App.Platform.Results",
"tr": "Sonuçlar",
"en": "Results"
},
{
"resourceName": "Platform",
"key": "App.Platform.Templates",
"tr": "Şablonlar",
"en": "Templates"
},
{
"resourceName": "Platform",
"key": "App.Platform.TemplateReplaceWarning",
"tr": "Şablon içeriği mevcut sorgu içeriğini değiştirecektir. Devam etmek istediğinizden emin misiniz?",
"en": "The template content will replace the current query content. Are you sure you want to continue?"
},
{
"resourceName": "Platform",
"key": "App.Platform.ConfirmTemplateReplace",
"tr": "Şablon Değişikliğini Onayla",
"en": "Confirm Template Replace"
},
{
"resourceName": "Platform",
"key": "App.Platform.TemplateLoaded",
"tr": "Şablon Yüklendi",
"en": "Template Loaded"
},
{
"resourceName": "Platform",
"key": "App.Platform.NoDataSourceSelected",
"tr": "Veri kaynağı seçilmedi",
"en": "No data source selected"
},
{
"resourceName": "Platform",
"key": "App.Platform.FailedToDeleteObject",
"tr": "Nesne silinemedi",
"en": "Failed to delete object"
},
{
"resourceName": "Platform",
"key": "App.Platform.ConfirmDelete",
"tr": "Silme Onayı",
"en": "Confirm Delete"
},
{
"resourceName": "Platform",
"key": "App.Platform.DeleteConfirmationMessage",
"tr": "Bu nesneyi silmek istediğinizden emin misiniz?",
"en": "Are you sure you want to delete this object?"
},
{
"resourceName": "Platform",
"key": "App.Platform.DeleteAction",
"tr": "Sil",
"en": "Delete"
}
]
}

View file

@ -296,6 +296,15 @@
"routeType": "protected",
"authority": []
},
{
"key": "admin.sqlQueryManager",
"path": "/admin/sqlQueryManager",
"componentPath": "@/views/sqlQueryManager/SqlQueryManager",
"routeType": "protected",
"authority": [
"App.SqlQueryManager"
]
},
{
"key": "admin.developerkit",
"path": "/admin/developerkit",
@ -1698,6 +1707,16 @@
"RequiredPermissionName": "App.DeveloperKit.DynamicServices",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "App.SqlQueryManager",
"DisplayName": "App.SqlQueryManager",
"Order": 9,
"Url": "/admin/sqlQueryManager",
"Icon": "FaDatabase",
"RequiredPermissionName": "App.SqlQueryManager",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "App.Intranet",

View file

@ -2263,6 +2263,15 @@
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.SqlQueryManager",
"ParentName": null,
"DisplayName": "App.SqlQueryManager",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit",

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Erp.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20251205085044_Initial")]
[Migration("20251205115506_Initial")]
partial class Initial
{
/// <inheritdoc />

View file

@ -0,0 +1,246 @@
import type { FullAuditedEntityDto, PagedAndSortedResultRequestDto } from '../index'
export enum SqlObjectType {
Query = 1,
StoredProcedure = 2,
View = 3,
Function = 4,
}
export enum SqlFunctionType {
ScalarFunction = 1,
TableValuedFunction = 2,
InlineTableValuedFunction = 3,
}
export enum SqlQueryStatus {
Draft = 1,
Active = 2,
Archived = 3,
}
// SQL Function DTOs
export interface SqlFunctionDto extends FullAuditedEntityDto<string> {
functionName: string
schemaName: string
displayName: string
description: string
functionType: SqlFunctionType
functionBody: string
returnType: string
dataSourceCode: string
status: SqlQueryStatus
category: string
isDeployed: boolean
lastDeployedAt?: string
parameters: string
}
export interface CreateSqlFunctionDto {
functionName: string
schemaName: string
displayName: string
description: string
functionType: SqlFunctionType
functionBody: string
returnType: string
dataSourceCode: string
category: string
parameters: string
}
export interface UpdateSqlFunctionDto {
displayName: string
description: string
functionBody: string
returnType: string
category: string
parameters: string
}
export interface DeployFunctionDto {
id: string
dropIfExists: boolean
}
// SQL Query DTOs
export interface SqlQueryDto extends FullAuditedEntityDto<string> {
code: string
name: string
description: string
queryText: string
dataSourceCode: string
status: SqlQueryStatus
category: string
tags: string
lastExecutedAt?: string
executionCount: number
isModifyingData: boolean
parameters: string
}
export interface CreateSqlQueryDto {
code: string
name: string
description: string
queryText: string
dataSourceCode: string
category: string
tags: string
isModifyingData: boolean
parameters: string
}
export interface UpdateSqlQueryDto {
code: string
name: string
description: string
queryText: string
dataSourceCode: string
category: string
tags: string
isModifyingData: boolean
parameters: string
}
export interface ExecuteSqlQueryDto {
queryText: string
dataSourceCode: string
parameters?: Record<string, any>
}
export interface ExecuteSavedQueryDto {
id: string
parameters?: Record<string, any>
}
export interface ValidateQueryDto {
queryText: string
dataSourceCode: string
}
// SQL Stored Procedure DTOs
export interface SqlStoredProcedureDto extends FullAuditedEntityDto<string> {
procedureName: string
schemaName: string
displayName: string
description: string
procedureBody: string
dataSourceCode: string
status: SqlQueryStatus
category: string
isDeployed: boolean
lastDeployedAt?: string
parameters: string
}
export interface CreateSqlStoredProcedureDto {
procedureName: string
schemaName: string
displayName: string
description: string
procedureBody: string
dataSourceCode: string
category: string
parameters: string
}
export interface UpdateSqlStoredProcedureDto {
displayName: string
description: string
procedureBody: string
category: string
parameters: string
}
export interface DeployStoredProcedureDto {
id: string
dropIfExists: boolean
}
// SQL View DTOs
export interface SqlViewDto extends FullAuditedEntityDto<string> {
viewName: string
schemaName: string
displayName: string
description: string
viewDefinition: string
dataSourceCode: string
status: SqlQueryStatus
category: string
isDeployed: boolean
lastDeployedAt?: string
withSchemaBinding: boolean
}
export interface CreateSqlViewDto {
viewName: string
schemaName: string
displayName: string
description: string
viewDefinition: string
dataSourceCode: string
category: string
withSchemaBinding: boolean
}
export interface UpdateSqlViewDto {
displayName: string
description: string
viewDefinition: string
category: string
withSchemaBinding: boolean
}
export interface DeployViewDto {
id: string
dropIfExists: boolean
}
// SQL Template DTOs
export interface SqlTemplateDto {
name: string
description: string
template: string
category: string
}
// SQL Execution Result
export interface SqlQueryExecutionResultDto {
success: boolean
message: string
executionTimeMs: number
rowsAffected: number
data?: any[]
metadata?: Record<string, any>
error?: string
}
// Request DTOs
export interface GetSqlFunctionsInput extends PagedAndSortedResultRequestDto {
filter?: string
dataSourceCode?: string
status?: SqlQueryStatus
category?: string
}
export interface GetSqlQueriesInput extends PagedAndSortedResultRequestDto {
filter?: string
dataSourceCode?: string
status?: SqlQueryStatus
category?: string
}
export interface GetSqlStoredProceduresInput extends PagedAndSortedResultRequestDto {
filter?: string
dataSourceCode?: string
status?: SqlQueryStatus
category?: string
}
export interface GetSqlViewsInput extends PagedAndSortedResultRequestDto {
filter?: string
dataSourceCode?: string
status?: SqlQueryStatus
category?: string
}

View file

@ -233,6 +233,9 @@ export const ROUTES_ENUM = {
bank: '/admin/accounting/bank',
checkNote: '/admin/accounting/check-note',
},
sqlManager: '/admin/sql-manager',
accessDenied: '/admin/access-denied',
},
}

View file

@ -1,6 +1,6 @@
import apiService, { Config } from '@/services/api.service'
import { PagedAndSortedResultRequestDto, PagedResultDto } from '../abp'
import type { DataSourceDto } from './models'
import { DataSourceDto } from '@/proxy/data-source'
import { PagedAndSortedResultRequestDto, PagedResultDto } from '@/proxy'
export class DataSourceService {
apiName = 'Default'

View file

@ -0,0 +1,442 @@
import apiService, { Config } from '@/services/api.service'
import type { PagedResultDto } from '@/proxy'
import type {
SqlFunctionDto,
CreateSqlFunctionDto,
UpdateSqlFunctionDto,
DeployFunctionDto,
SqlQueryDto,
CreateSqlQueryDto,
UpdateSqlQueryDto,
ExecuteSqlQueryDto,
ExecuteSavedQueryDto,
ValidateQueryDto,
SqlStoredProcedureDto,
CreateSqlStoredProcedureDto,
UpdateSqlStoredProcedureDto,
DeployStoredProcedureDto,
SqlViewDto,
CreateSqlViewDto,
UpdateSqlViewDto,
DeployViewDto,
SqlTemplateDto,
SqlQueryExecutionResultDto,
GetSqlFunctionsInput,
GetSqlQueriesInput,
GetSqlStoredProceduresInput,
GetSqlViewsInput,
} from '@/proxy/sql-query-manager/models'
export class SqlFunctionService {
apiName = 'Default'
create = (input: CreateSqlFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, CreateSqlFunctionDto>(
{
method: 'POST',
url: '/api/app/sql-function',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlFunctionsInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlFunctionDto>, GetSqlFunctionsInput>(
{
method: 'GET',
url: '/api/app/sql-function',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, void>(
{
method: 'GET',
url: `/api/app/sql-function/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlFunctionDto, UpdateSqlFunctionDto>(
{
method: 'PUT',
url: `/api/app/sql-function/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-function/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployFunctionDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployFunctionDto>(
{
method: 'POST',
url: '/api/app/sql-function/deploy',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
{
method: 'POST',
url: `/api/app/sql-function/${id}/check-exists`,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-function/${id}/drop`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlQueryService {
apiName = 'Default'
create = (input: CreateSqlQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, CreateSqlQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlQueriesInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlQueryDto>, GetSqlQueriesInput>(
{
method: 'GET',
url: '/api/app/sql-query',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, void>(
{
method: 'GET',
url: `/api/app/sql-query/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryDto, UpdateSqlQueryDto>(
{
method: 'PUT',
url: `/api/app/sql-query/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-query/${id}`,
},
{ apiName: this.apiName, ...config },
)
executeQuery = (input: ExecuteSqlQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, ExecuteSqlQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query/execute-query',
data: input,
},
{ apiName: this.apiName, ...config },
)
executeSavedQuery = (id: string, parameters?: Record<string, any>, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, ExecuteSavedQueryDto>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/execute-saved-query`,
data: { id, parameters },
},
{ apiName: this.apiName, ...config },
)
validateQuery = (input: ValidateQueryDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, ValidateQueryDto>(
{
method: 'POST',
url: '/api/app/sql-query/validate-query',
data: input,
},
{ apiName: this.apiName, ...config },
)
activate = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/activate`,
},
{ apiName: this.apiName, ...config },
)
archive = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'POST',
url: `/api/app/sql-query/${id}/archive`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlStoredProcedureService {
apiName = 'Default'
create = (input: CreateSqlStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, CreateSqlStoredProcedureDto>(
{
method: 'POST',
url: '/api/app/sql-stored-procedure',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlStoredProceduresInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlStoredProcedureDto>, GetSqlStoredProceduresInput>(
{
method: 'GET',
url: '/api/app/sql-stored-procedure',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, void>(
{
method: 'GET',
url: `/api/app/sql-stored-procedure/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlStoredProcedureDto, UpdateSqlStoredProcedureDto>(
{
method: 'PUT',
url: `/api/app/sql-stored-procedure/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-stored-procedure/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployStoredProcedureDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployStoredProcedureDto>(
{
method: 'POST',
url: '/api/app/sql-stored-procedure/deploy',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
{
method: 'POST',
url: `/api/app/sql-stored-procedure/${id}/check-exists`,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-stored-procedure/${id}/drop`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlViewService {
apiName = 'Default'
create = (input: CreateSqlViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, CreateSqlViewDto>(
{
method: 'POST',
url: '/api/app/sql-view',
data: input,
},
{ apiName: this.apiName, ...config },
)
getList = (input: GetSqlViewsInput, config?: Partial<Config>) =>
apiService.fetchData<PagedResultDto<SqlViewDto>, GetSqlViewsInput>(
{
method: 'GET',
url: '/api/app/sql-view',
params: input,
},
{ apiName: this.apiName, ...config },
)
get = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, void>(
{
method: 'GET',
url: `/api/app/sql-view/${id}`,
},
{ apiName: this.apiName, ...config },
)
update = (id: string, input: UpdateSqlViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlViewDto, UpdateSqlViewDto>(
{
method: 'PUT',
url: `/api/app/sql-view/${id}`,
data: input,
},
{ apiName: this.apiName, ...config },
)
delete = (id: string, config?: Partial<Config>) =>
apiService.fetchData<void, void>(
{
method: 'DELETE',
url: `/api/app/sql-view/${id}`,
},
{ apiName: this.apiName, ...config },
)
deploy = (input: DeployViewDto, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, DeployViewDto>(
{
method: 'POST',
url: '/api/app/sql-view/deploy',
data: input,
},
{ apiName: this.apiName, ...config },
)
checkExists = (id: string, config?: Partial<Config>) =>
apiService.fetchData<boolean, void>(
{
method: 'POST',
url: `/api/app/sql-view/${id}/check-exists`,
},
{ apiName: this.apiName, ...config },
)
drop = (id: string, config?: Partial<Config>) =>
apiService.fetchData<SqlQueryExecutionResultDto, void>(
{
method: 'POST',
url: `/api/app/sql-view/${id}/drop`,
},
{ apiName: this.apiName, ...config },
)
}
export class SqlTemplateService {
apiName = 'Default'
getQueryTemplates = (config?: Partial<Config>) =>
apiService.fetchData<SqlTemplateDto[], void>(
{
method: 'GET',
url: '/api/app/sql-template/query-templates',
},
{ apiName: this.apiName, ...config },
)
getStoredProcedureTemplate = (
procedureName: string,
schemaName = 'dbo',
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/stored-procedure-template',
params: { procedureName, schemaName },
},
{ apiName: this.apiName, ...config },
)
getViewTemplate = (
viewName: string,
schemaName = 'dbo',
withSchemaBinding = false,
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/view-template',
params: { viewName, schemaName, withSchemaBinding },
},
{ apiName: this.apiName, ...config },
)
getFunctionTemplate = (
functionName: string,
functionType: number,
schemaName = 'dbo',
config?: Partial<Config>,
) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/function-template',
params: { functionName, functionType, schemaName },
},
{ apiName: this.apiName, ...config },
)
getQueryTemplate = (templateType: string, config?: Partial<Config>) =>
apiService.fetchData<string, void>(
{
method: 'GET',
url: '/api/app/sql-template/query-template',
params: { templateType },
},
{ apiName: this.apiName, ...config },
)
}
// Export service instances
export const sqlFunctionService = new SqlFunctionService()
export const sqlQueryService = new SqlQueryService()
export const sqlStoredProcedureService = new SqlStoredProcedureService()
export const sqlViewService = new SqlViewService()
export const sqlTemplateService = new SqlTemplateService()

View file

@ -0,0 +1,712 @@
import { useState, useCallback, useEffect } from 'react'
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import AdaptableCard from '@/components/shared/AdaptableCard'
import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
SqlQueryDto,
SqlStoredProcedureDto,
SqlViewDto,
SqlObjectType,
SqlQueryExecutionResultDto,
} from '@/proxy/sql-query-manager/models'
import {
sqlFunctionService,
sqlQueryService,
sqlStoredProcedureService,
sqlViewService,
} from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaSave, FaSyncAlt } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer'
import SqlEditor from './components/SqlEditor'
import SqlResultsGrid from './components/SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties'
import { FaCloudUploadAlt } from 'react-icons/fa'
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
interface SqlManagerState {
dataSources: DataSourceDto[]
selectedDataSource: DataSourceDto | null
selectedObject: SqlObject | null
selectedObjectType: SqlObjectType | null
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
}
const SqlQueryManager = () => {
const { translate } = useLocalization()
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
selectedDataSource: null,
selectedObject: null,
selectedObjectType: null,
editorContent:
'-- SQL Query Editor\n-- Write your SQL query here and press F5 or click Execute to run\n\nSELECT * FROM YourTable\nWHERE 1=1',
isExecuting: false,
executionResult: null,
showProperties: false,
isDirty: false,
})
const [showSaveDialog, setShowSaveDialog] = useState(false)
const [saveDialogData, setSaveDialogData] = useState({ name: '', description: '' })
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(null)
useEffect(() => {
loadDataSources()
}, [])
const loadDataSources = async () => {
try {
const response = await getDataSources()
const items = response.data.items || []
if (items.length > 0) {
setState((prev) => ({
...prev,
dataSources: items,
selectedDataSource: items[0],
}))
}
} catch (error) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{translate('::App.Platform.FailedtoloadDatasources')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
...prev,
selectedDataSource: dataSource,
selectedObject: null,
editorContent: '',
executionResult: null,
isDirty: false,
}))
}, [])
const handleObjectSelect = useCallback(
(object: SqlObject | null, objectType: SqlObjectType | null) => {
if (state.isDirty) {
if (!confirm(translate('::App.Platform.UnsavedChangesConfirmation'))) {
return
}
}
let content = ''
if (object) {
if (objectType === 1) {
// Query
content = (object as SqlQueryDto).queryText
} else if (objectType === 2) {
// Stored Procedure
content = (object as SqlStoredProcedureDto).procedureBody
} else if (objectType === 3) {
// View
content = (object as SqlViewDto).viewDefinition
} else if (objectType === 4) {
// Function
content = (object as SqlFunctionDto).functionBody
}
}
setState((prev) => ({
...prev,
selectedObject: object,
selectedObjectType: objectType,
editorContent: content,
executionResult: null,
isDirty: false,
}))
},
[state.isDirty, translate],
)
const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({
...prev,
editorContent: value || '',
isDirty: true,
}))
}, [])
const getTemplateContent = (templateType: string): string => {
const templates: Record<string, string> = {
'select': `-- Basic SELECT query
SELECT
Column1,
Column2,
Column3
FROM
TableName
WHERE
-- Add your conditions
Column1 = 'value'
AND IsActive = 1
ORDER BY
Column1 ASC;`,
'insert': `-- Basic INSERT query
INSERT INTO TableName (Column1, Column2, Column3)
VALUES
('Value1', 'Value2', 'Value3');`,
'update': `-- Basic UPDATE query
UPDATE TableName
SET
Column1 = 'NewValue1',
Column2 = 'NewValue2'
WHERE
-- Add your conditions
Id = 1;`,
'delete': `-- Basic DELETE query
DELETE FROM TableName
WHERE
-- Add your conditions
Id = 1;`,
'create-procedure': `-- Create Stored Procedure
CREATE PROCEDURE [dbo].[ProcedureName]
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
-- Add your logic here
SELECT
Column1,
Column2
FROM
TableName
WHERE
Column1 = @Parameter1
AND Column2 = @Parameter2;
END
GO`,
'create-view': `-- Create View
CREATE VIEW [dbo].[ViewName]
AS
SELECT
t1.Column1,
t1.Column2,
t2.Column3
FROM
TableName1 t1
INNER JOIN TableName2 t2 ON t1.Id = t2.TableName1Id
WHERE
t1.IsActive = 1;
GO`,
'create-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[FunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS NVARCHAR(200)
AS
BEGIN
DECLARE @Result NVARCHAR(200);
-- Add your logic here
SELECT @Result = Column1 + ' ' + @Parameter2
FROM TableName
WHERE Id = @Parameter1;
RETURN @Result;
END
GO`
}
return templates[templateType] || templates['select']
}
const applyTemplate = useCallback((templateContent: string) => {
setState((prev) => ({
...prev,
editorContent: templateContent,
selectedObject: null,
selectedObjectType: null,
executionResult: null,
isDirty: false,
}))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.TemplateLoaded')}
</Notification>,
{ placement: 'top-center' },
)
}, [translate])
const handleTemplateSelect = useCallback((template: string, templateType: string) => {
const templateContent = getTemplateContent(templateType)
// Check if editor has content and it's not from a previous template
const hasUserContent = state.editorContent.trim() && state.isDirty
if (hasUserContent) {
// Ask for confirmation
setPendingTemplate({ content: templateContent, type: templateType })
setShowTemplateConfirmDialog(true)
} else {
// Apply template directly
applyTemplate(templateContent)
}
}, [translate, state.editorContent, state.isDirty, applyTemplate])
const handleConfirmTemplateReplace = useCallback(() => {
if (pendingTemplate) {
applyTemplate(pendingTemplate.content)
}
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [pendingTemplate, applyTemplate])
const handleCancelTemplateReplace = useCallback(() => {
setShowTemplateConfirmDialog(false)
setPendingTemplate(null)
}, [])
const handleExecute = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.editorContent.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterQuery')}
</Notification>,
{ placement: 'top-center' },
)
return
}
setState((prev) => ({ ...prev, isExecuting: true, executionResult: null }))
try {
const result = await sqlQueryService.executeQuery({
queryText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
})
setState((prev) => ({ ...prev, executionResult: result.data, isExecuting: false }))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms)
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
setState((prev) => ({ ...prev, isExecuting: false }))
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleSave = async () => {
if (!state.selectedDataSource) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectDataSource')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.editorContent.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseEnterContentToSave')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (state.selectedObject && state.selectedObjectType) {
// Update existing object
await handleUpdate()
} else {
// Create new object - show dialog to choose type
setSaveDialogData({ name: '', description: '' })
setShowSaveDialog(true)
}
}
const handleUpdate = async () => {
if (!state.selectedObject || !state.selectedObjectType || !state.selectedDataSource) return
if (!state.selectedObject.id) return
try {
const objectId = state.selectedObject.id
switch (state.selectedObjectType) {
case 1: // Query
await sqlQueryService.update(objectId, {
...(state.selectedObject as SqlQueryDto),
queryText: state.editorContent,
})
break
case 2: // Stored Procedure
await sqlStoredProcedureService.update(objectId, {
displayName: (state.selectedObject as SqlStoredProcedureDto).displayName,
description: (state.selectedObject as SqlStoredProcedureDto).description,
procedureBody: state.editorContent,
category: (state.selectedObject as SqlStoredProcedureDto).category,
parameters: (state.selectedObject as SqlStoredProcedureDto).parameters,
})
break
case 3: // View
await sqlViewService.update(objectId, {
displayName: (state.selectedObject as SqlViewDto).displayName,
description: (state.selectedObject as SqlViewDto).description,
viewDefinition: state.editorContent,
category: (state.selectedObject as SqlViewDto).category,
withSchemaBinding: (state.selectedObject as SqlViewDto).withSchemaBinding,
})
break
case 4: // Function
await sqlFunctionService.update(objectId, {
displayName: (state.selectedObject as SqlFunctionDto).displayName,
description: (state.selectedObject as SqlFunctionDto).description,
functionBody: state.editorContent,
returnType: (state.selectedObject as SqlFunctionDto).returnType,
category: (state.selectedObject as SqlFunctionDto).category,
parameters: (state.selectedObject as SqlFunctionDto).parameters,
})
break
}
setState((prev) => ({ ...prev, isDirty: false }))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectUpdatedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleCreateNewQuery = async () => {
if (!state.selectedDataSource || !saveDialogData.name) return
try {
await sqlQueryService.create({
code: saveDialogData.name.replace(/\s+/g, '_'),
name: saveDialogData.name,
description: saveDialogData.description,
queryText: state.editorContent,
dataSourceCode: state.selectedDataSource.code || '',
category: '',
tags: '',
isModifyingData: false,
parameters: '',
})
setState((prev) => ({ ...prev, isDirty: false }))
setShowSaveDialog(false)
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.QuerySavedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToSaveQuery')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleDeploy = async () => {
if (!state.selectedObject || !state.selectedObjectType) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.PleaseSelectAnObjectToDeploy')}
</Notification>,
{ placement: 'top-center' },
)
return
}
if (!state.selectedObject.id) return
try {
const objectId = state.selectedObject.id
let result: any
switch (state.selectedObjectType) {
case 2: // Stored Procedure
result = await sqlStoredProcedureService.deploy({ id: objectId, dropIfExists: true })
break
case 3: // View
result = await sqlViewService.deploy({ id: objectId, dropIfExists: true })
break
case 4: // Function
result = await sqlFunctionService.deploy({ id: objectId, dropIfExists: true })
break
default:
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.ThisObjectTypeCannotBeDeployed')}
</Notification>,
{ placement: 'top-center' },
)
return
}
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectDeployedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToDeployObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
return (
<Container className="h-full overflow-hidden">
<div className="flex flex-col h-full gap-4 p-1">
{/* Toolbar */}
<AdaptableCard className="flex-shrink-0 shadow-sm">
<div className="flex items-center justify-between px-1 py-1">
<div className="flex items-center gap-3">
<FaDatabase className="text-lg text-blue-500" />
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{translate('::App.Platform.DataSource')}:
</span>
<select
className="border border-gray-300 rounded px-1 py-1"
value={state.selectedDataSource?.code || ''}
onChange={(e) => {
const ds = state.dataSources.find((d) => d.code === e.target.value)
if (ds) handleDataSourceChange(ds)
}}
>
{state.dataSources.map((ds) => (
<option key={ds.code} value={ds.code}>
{ds.code}
</option>
))}
</select>
</div>
<div className="flex items-center gap-3">
<Button
size="sm"
variant="solid"
color="blue-600"
icon={<FaPlay />}
onClick={handleExecute}
loading={state.isExecuting}
disabled={!state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Execute')}
<span className="ml-1 text-xs opacity-75">(F5)</span>
</Button>
<Button
size="sm"
variant="solid"
icon={<FaSave />}
onClick={handleSave}
disabled={!state.isDirty || !state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Save')}
<span className="ml-1 text-xs opacity-75">(Ctrl+S)</span>
</Button>
{state.selectedObject &&
state.selectedObjectType &&
state.selectedObjectType !== 1 && (
<Button
size="sm"
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={handleDeploy}
disabled={!state.selectedDataSource}
className="shadow-sm"
>
{translate('::App.Platform.Deploy')}
</Button>
)}
<Button
size="sm"
variant="twoTone"
icon={<FaSyncAlt />}
onClick={() => window.location.reload()}
className="shadow-sm"
>
{translate('::App.Platform.Refresh')}
</Button>
</div>
</div>
</AdaptableCard>
{/* Main Content Area */}
<div className="flex-1 flex gap-4 min-h-0">
{/* Left Panel - Object Explorer */}
<div className="w-80 flex-shrink-0 flex flex-col min-h-0">
<AdaptableCard className="h-full" bodyClass="p-0">
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
<div className="flex-1 overflow-auto p-2 min-h-0">
<SqlObjectExplorer
dataSource={state.selectedDataSource}
onObjectSelect={handleObjectSelect}
selectedObject={state.selectedObject}
onTemplateSelect={handleTemplateSelect}
/>
</div>
</div>
</AdaptableCard>
</div>
{/* Center Panel - Editor and Results */}
<div className="flex-1 flex flex-col min-h-0">
<div className="flex-1 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">{translate('::App.Platform.QueryEditor')}</h6>
</div>
<div className="flex-1 min-h-0">
<SqlEditor
value={state.editorContent}
onChange={handleEditorChange}
onExecute={handleExecute}
onSave={handleSave}
readOnly={state.isExecuting}
/>
</div>
</div>
{state.executionResult && (
<div className="flex-1 mt-4 border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col overflow-hidden">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
<h6 className="font-semibold text-sm">{translate('::App.Platform.Results')}</h6>
</div>
<div className="flex-1 overflow-hidden min-h-0">
<SqlResultsGrid result={state.executionResult} />
</div>
</div>
)}
</div>
{/* Right Panel - Properties (Optional) */}
{state.showProperties && state.selectedObject && (
<div className="w-80 flex-shrink-0 flex flex-col min-h-0">
<SqlObjectProperties object={state.selectedObject} type={state.selectedObjectType} />
</div>
)}
</div>
</div>
{/* Template Confirmation Dialog */}
<Dialog
isOpen={showTemplateConfirmDialog}
onClose={handleCancelTemplateReplace}
onRequestClose={handleCancelTemplateReplace}
>
<h5 className="mb-4">{translate('::App.Platform.ConfirmTemplateReplace')}</h5>
<p className="mb-6 text-gray-600 dark:text-gray-400">
{translate('::App.Platform.TemplateReplaceWarning')}
</p>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={handleCancelTemplateReplace}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" onClick={handleConfirmTemplateReplace}>
{translate('::App.Platform.Replace')}
</Button>
</div>
</Dialog>
{/* Save Dialog */}
<Dialog
isOpen={showSaveDialog}
onClose={() => setShowSaveDialog(false)}
onRequestClose={() => setShowSaveDialog(false)}
>
<h5 className="mb-4">{translate('::App.Platform.SaveAsNewQuery')}</h5>
<div className="space-y-4">
<div>
<label className="block mb-2">{translate('::App.Platform.Name')}</label>
<Input
value={saveDialogData.name}
onChange={(e) => setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<label className="block mb-2">{translate('::App.Platform.Description')}</label>
<Input
value={saveDialogData.description}
onChange={(e) =>
setSaveDialogData((prev) => ({ ...prev, description: e.target.value }))
}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setShowSaveDialog(false)}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" onClick={handleCreateNewQuery}>
{translate('::App.Platform.Save')}
</Button>
</div>
</div>
</Dialog>
</Container>
)
}
export default SqlQueryManager

View file

@ -0,0 +1,216 @@
import { useEffect, useRef } from 'react'
import Editor, { Monaco } from '@monaco-editor/react'
import { useConfig } from '@/components/ui/ConfigProvider'
import type { editor } from 'monaco-editor'
interface SqlEditorProps {
value: string
onChange: (value: string | undefined) => void
onExecute?: () => void
onSave?: () => void
readOnly?: boolean
height?: string
}
const SqlEditor = ({
value,
onChange,
onExecute,
onSave,
readOnly = false,
height = '100%',
}: SqlEditorProps) => {
const { mode } = useConfig()
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<Monaco | null>(null)
const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor
monacoRef.current = monaco
// Add keyboard shortcuts
editor.addCommand(monaco.KeyCode.F5, () => {
if (onExecute) onExecute()
})
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
if (onSave) onSave()
})
// Configure SQL language features
monaco.languages.registerCompletionItemProvider('sql', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
const suggestions = [
// SQL Keywords
{
label: 'SELECT',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'SELECT ',
range: range,
},
{
label: 'FROM',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'FROM ',
range: range,
},
{
label: 'WHERE',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'WHERE ',
range: range,
},
{
label: 'INSERT INTO',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'INSERT INTO ',
range: range,
},
{
label: 'UPDATE',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'UPDATE ',
range: range,
},
{
label: 'DELETE FROM',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'DELETE FROM ',
range: range,
},
{
label: 'CREATE TABLE',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'CREATE TABLE ',
range: range,
},
{
label: 'CREATE PROCEDURE',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'CREATE PROCEDURE ${1:ProcedureName}\nAS\nBEGIN\n\t$0\nEND',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range,
},
{
label: 'CREATE FUNCTION',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText:
'CREATE FUNCTION ${1:FunctionName}()\nRETURNS ${2:ReturnType}\nAS\nBEGIN\n\t$0\nEND',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range,
},
{
label: 'CREATE VIEW',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'CREATE VIEW ${1:ViewName}\nAS\n\tSELECT $0',
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range: range,
},
{
label: 'JOIN',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'JOIN ',
range: range,
},
{
label: 'LEFT JOIN',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'LEFT JOIN ',
range: range,
},
{
label: 'RIGHT JOIN',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'RIGHT JOIN ',
range: range,
},
{
label: 'INNER JOIN',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'INNER JOIN ',
range: range,
},
{
label: 'GROUP BY',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'GROUP BY ',
range: range,
},
{
label: 'ORDER BY',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'ORDER BY ',
range: range,
},
{
label: 'HAVING',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'HAVING ',
range: range,
},
]
return { suggestions: suggestions }
},
})
// Focus the editor
editor.focus()
}
const editorOptions: editor.IStandaloneEditorConstructionOptions = {
readOnly: readOnly,
minimap: { enabled: true },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on',
folding: true,
glyphMargin: true,
lineDecorationsWidth: 10,
lineNumbersMinChars: 3,
renderLineHighlight: 'all',
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
useShadows: false,
verticalHasArrows: false,
horizontalHasArrows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
},
suggest: {
showKeywords: true,
showSnippets: true,
},
}
return (
<div className="h-full w-full relative">
<div className="absolute inset-0">
<Editor
height="100%"
defaultLanguage="sql"
value={value}
onChange={onChange}
onMount={handleEditorDidMount}
theme={mode === 'dark' ? 'vs-dark' : 'light'}
options={editorOptions}
/>
</div>
</div>
)
}
export default SqlEditor

View file

@ -0,0 +1,495 @@
import { useState, useEffect, useCallback } from 'react'
import { Dialog, Button, Notification, toast } from '@/components/ui'
import {
FaRegFolder,
FaRegFolderOpen,
FaRegFileAlt,
FaCog,
FaColumns,
FaCode,
FaSyncAlt,
FaEdit,
FaTrash,
} from 'react-icons/fa'
import type { DataSourceDto } from '@/proxy/data-source'
import type {
SqlFunctionDto,
SqlQueryDto,
SqlStoredProcedureDto,
SqlViewDto,
SqlObjectType,
} from '@/proxy/sql-query-manager/models'
import {
sqlFunctionService,
sqlQueryService,
sqlStoredProcedureService,
sqlViewService,
} from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
interface TreeNode {
id: string
label: string
type: 'root' | 'folder' | 'object'
objectType?: SqlObjectType
data?: SqlObject
children?: TreeNode[]
expanded?: boolean
}
interface SqlObjectExplorerProps {
dataSource: DataSourceDto | null
onObjectSelect: (object: SqlObject | null, objectType: SqlObjectType | null) => void
selectedObject: SqlObject | null
onTemplateSelect?: (template: string, templateType: string) => void
}
const SqlObjectExplorer = ({
dataSource,
onObjectSelect,
selectedObject,
onTemplateSelect,
}: SqlObjectExplorerProps) => {
const { translate } = useLocalization()
const [treeData, setTreeData] = useState<TreeNode[]>([])
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(
new Set(['root', 'templates', 'queries', 'storedProcedures', 'views', 'functions']),
)
const [loading, setLoading] = useState(false)
const [contextMenu, setContextMenu] = useState<{
show: boolean
x: number
y: number
node: TreeNode | null
}>({ show: false, x: 0, y: 0, node: null })
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [objectToDelete, setObjectToDelete] = useState<{
object: SqlObject
type: SqlObjectType
} | null>(null)
useEffect(() => {
if (dataSource) {
loadObjects()
} else {
setTreeData([])
}
}, [dataSource])
const loadObjects = async () => {
if (!dataSource) return
setLoading(true)
try {
const [queries, storedProcedures, views, functions] = await Promise.all([
sqlQueryService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlStoredProcedureService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlViewService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
sqlFunctionService.getList({
skipCount: 0,
maxResultCount: 1000,
dataSourceCode: dataSource.code,
}),
])
const tree: TreeNode[] = [
{
id: 'root',
label: dataSource.code || 'Database',
type: 'root',
expanded: true,
children: [
{
id: 'templates',
label: translate('::App.Platform.Templates'),
type: 'folder',
expanded: expandedNodes.has('templates'),
children: [
{
id: 'template-select',
label: 'SELECT Query',
type: 'object' as const,
data: { templateType: 'select' } as any,
},
{
id: 'template-insert',
label: 'INSERT Query',
type: 'object' as const,
data: { templateType: 'insert' } as any,
},
{
id: 'template-update',
label: 'UPDATE Query',
type: 'object' as const,
data: { templateType: 'update' } as any,
},
{
id: 'template-delete',
label: 'DELETE Query',
type: 'object' as const,
data: { templateType: 'delete' } as any,
},
{
id: 'template-sp',
label: 'Stored Procedure',
type: 'object' as const,
data: { templateType: 'create-procedure' } as any,
},
{
id: 'template-view',
label: 'View',
type: 'object' as const,
data: { templateType: 'create-view' } as any,
},
{
id: 'template-function',
label: 'Function',
type: 'object' as const,
data: { templateType: 'create-function' } as any,
},
],
},
{
id: 'queries',
label: `${translate('::App.Platform.Queries')} (${queries.data.totalCount})`,
type: 'folder',
objectType: 1,
expanded: expandedNodes.has('queries'),
children:
queries.data.items?.map((q) => ({
id: q.id || '',
label: q.name,
type: 'object' as const,
objectType: 1 as SqlObjectType,
data: q,
})) || [],
},
{
id: 'storedProcedures',
label: `${translate('::App.Platform.StoredProcedures')} (${storedProcedures.data.totalCount})`,
type: 'folder',
objectType: 2,
expanded: expandedNodes.has('storedProcedures'),
children:
storedProcedures.data.items?.map((sp) => ({
id: sp.id || '',
label: sp.displayName || sp.procedureName,
type: 'object' as const,
objectType: 2 as SqlObjectType,
data: sp,
})) || [],
},
{
id: 'views',
label: `${translate('::App.Platform.Views')} (${views.data.totalCount})`,
type: 'folder',
objectType: 3,
expanded: expandedNodes.has('views'),
children:
views.data.items?.map((v) => ({
id: v.id || '',
label: v.displayName || v.viewName,
type: 'object' as const,
objectType: 3 as SqlObjectType,
data: v,
})) || [],
},
{
id: 'functions',
label: `${translate('::App.Platform.Functions')} (${functions.data.totalCount})`,
type: 'folder',
objectType: 4,
expanded: expandedNodes.has('functions'),
children:
functions.data.items?.map((f) => ({
id: f.id || '',
label: f.displayName || f.functionName,
type: 'object' as const,
objectType: 4 as SqlObjectType,
data: f,
})) || [],
},
],
},
]
setTreeData(tree)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadObjects')}
</Notification>,
{ placement: 'top-center' },
)
} finally {
setLoading(false)
}
}
const toggleNode = (nodeId: string) => {
setExpandedNodes((prev) => {
const newSet = new Set(prev)
if (newSet.has(nodeId)) newSet.delete(nodeId)
else newSet.add(nodeId)
return newSet
})
}
const handleNodeClick = (node: TreeNode) => {
if (node.type === 'folder' || node.type === 'root') {
toggleNode(node.id)
} else if (node.type === 'object' && node.data) {
// Check if it's a template
if ((node.data as any).templateType && onTemplateSelect) {
const templateType = (node.data as any).templateType
onTemplateSelect('', templateType) // Template content will be generated in parent
} else if (node.objectType) {
onObjectSelect(node.data, node.objectType)
}
}
}
const handleContextMenu = (e: React.MouseEvent, node: TreeNode) => {
e.preventDefault()
setContextMenu({
show: true,
x: e.clientX,
y: e.clientY,
node,
})
}
const handleDelete = async () => {
if (!objectToDelete || !objectToDelete.object.id) return
try {
const { object, type } = objectToDelete
switch (type) {
case 1:
await sqlQueryService.delete(object.id!)
break
case 2:
await sqlStoredProcedureService.delete(object.id!)
break
case 3:
await sqlViewService.delete(object.id!)
break
case 4:
await sqlFunctionService.delete(object.id!)
break
}
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{translate('::App.Platform.ObjectDeletedSuccessfully')}
</Notification>,
{ placement: 'top-center' },
)
setShowDeleteDialog(false)
setObjectToDelete(null)
loadObjects()
if (selectedObject?.id === object.id) {
onObjectSelect(null, null)
}
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || translate('::App.Platform.FailedToDeleteObject')}
</Notification>,
{ placement: 'top-center' },
)
}
}
const getIcon = (node: TreeNode) => {
if (node.type === 'root') return <FaRegFolder className="text-blue-500" />
if (node.type === 'folder') {
const isExpanded = expandedNodes.has(node.id)
// Templates folder
if (node.id === 'templates')
return isExpanded ? (
<FaRegFolderOpen className="text-orange-500" />
) : (
<FaRegFolder className="text-orange-500" />
)
if (node.objectType === 1)
return isExpanded ? (
<FaRegFolderOpen className="text-yellow-500" />
) : (
<FaRegFolder className="text-yellow-500" />
)
if (node.objectType === 2)
return isExpanded ? (
<FaRegFolderOpen className="text-green-500" />
) : (
<FaRegFolder className="text-green-500" />
)
if (node.objectType === 3)
return isExpanded ? (
<FaRegFolderOpen className="text-purple-500" />
) : (
<FaRegFolder className="text-purple-500" />
)
if (node.objectType === 4)
return isExpanded ? (
<FaRegFolderOpen className="text-red-500" />
) : (
<FaRegFolder className="text-red-500" />
)
}
if (node.type === 'object') {
// Check if it's a template
if ((node.data as any)?.templateType) {
return <FaCode className="text-orange-500" />
}
if (node.objectType === 1) return <FaRegFileAlt className="text-gray-500" />
if (node.objectType === 2) return <FaCog className="text-gray-500" />
if (node.objectType === 3) return <FaColumns className="text-gray-500" />
if (node.objectType === 4) return <FaCode className="text-gray-500" />
}
return <FaRegFolder />
}
const renderNode = (node: TreeNode, level = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isSelected = node.type === 'object' && selectedObject?.id === node.id
return (
<div key={node.id}>
<div
className={`flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded ${
isSelected ? 'bg-blue-100 dark:bg-blue-900' : ''
}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onClick={() => handleNodeClick(node)}
onContextMenu={(e) => handleContextMenu(e, node)}
>
{getIcon(node)}
<span className="text-sm flex-1">{node.label}</span>
</div>
{isExpanded && node.children && (
<div>{node.children.map((child) => renderNode(child, level + 1))}</div>
)}
</div>
)
}
return (
<div className="h-full">
{loading && <div className="text-center py-8 text-gray-500">{translate('::App.Platform.Loading')}</div>}
{!loading && treeData.length === 0 && (
<div className="text-center py-8 text-gray-500">
{translate('::App.Platform.NoDataSourceSelected')}
</div>
)}
{!loading && treeData.length > 0 && (
<div className="space-y-1">{treeData.map((node) => renderNode(node))}</div>
)}
{contextMenu.show && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setContextMenu({ show: false, x: 0, y: 0, node: null })}
/>
<div
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
{contextMenu.node?.type === 'object' && (
<>
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
onClick={() => {
if (contextMenu.node?.data && contextMenu.node?.objectType) {
onObjectSelect(contextMenu.node.data, contextMenu.node.objectType)
}
setContextMenu({ show: false, x: 0, y: 0, node: null })
}}
>
<FaEdit className="inline mr-2" />
{translate('::App.Platform.Edit')}
</button>
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-red-600"
onClick={() => {
if (contextMenu.node?.data && contextMenu.node?.objectType) {
setObjectToDelete({
object: contextMenu.node.data,
type: contextMenu.node.objectType,
})
setShowDeleteDialog(true)
}
setContextMenu({ show: false, x: 0, y: 0, node: null })
}}
>
<FaTrash className="inline mr-2" />
{translate('::App.Platform.Delete')}
</button>
</>
)}
{contextMenu.node?.type === 'folder' && (
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm"
onClick={() => {
loadObjects()
setContextMenu({ show: false, x: 0, y: 0, node: null })
}}
>
<FaSyncAlt className="inline mr-2" />
{translate('::App.Platform.Refresh')}
</button>
)}
</div>
</>
)}
<Dialog
isOpen={showDeleteDialog}
onClose={() => setShowDeleteDialog(false)}
onRequestClose={() => setShowDeleteDialog(false)}
>
<h5 className="mb-4">{translate('::App.Platform.ConfirmDelete')}</h5>
<p className="mb-4">{translate('::App.Platform.DeleteConfirmationMessage')}</p>
<div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setShowDeleteDialog(false)}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" onClick={handleDelete}>
{translate('::App.Platform.DeleteAction')}
</Button>
</div>
</Dialog>
</div>
)
}
export default SqlObjectExplorer

View file

@ -0,0 +1,214 @@
import type {
SqlFunctionDto,
SqlQueryDto,
SqlStoredProcedureDto,
SqlViewDto,
SqlObjectType,
} from '@/proxy/sql-query-manager/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs'
export type SqlObject = SqlFunctionDto | SqlQueryDto | SqlStoredProcedureDto | SqlViewDto
interface SqlObjectPropertiesProps {
object: SqlObject | null
type: SqlObjectType | null
}
const SqlObjectProperties = ({ object, type }: SqlObjectPropertiesProps) => {
const { translate } = useLocalization()
if (!object || !type) {
return (
<div className="p-4 text-center text-gray-500">
{translate('::App.Platform.SelectAnObjectToViewProperties')}
</div>
)
}
const PropertyRow = ({ label, value }: { label: string; value: any }) => (
<div className="mb-3">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{label}</div>
<div className="text-sm font-medium">{value || '-'}</div>
</div>
)
const getObjectTypeName = () => {
switch (type) {
case 1:
return translate('::App.Platform.Query')
case 2:
return translate('::App.Platform.StoredProcedure')
case 3:
return translate('::App.Platform.View')
case 4:
return translate('::App.Platform.Function')
default:
return translate('::App.Platform.Object')
}
}
const getStatusBadge = (status: number) => {
switch (status) {
case 1:
return <span className="px-2 py-1 text-xs rounded bg-gray-200 dark:bg-gray-700">{translate('::App.Platform.Draft')}</span>
case 2:
return <span className="px-2 py-1 text-xs rounded bg-green-200 dark:bg-green-700">{translate('::App.Platform.Active')}</span>
case 3:
return <span className="px-2 py-1 text-xs rounded bg-orange-200 dark:bg-orange-700">{translate('::App.Platform.Archived')}</span>
default:
return <span className="px-2 py-1 text-xs rounded bg-gray-200 dark:bg-gray-700">{translate('::App.Platform.Unknown')}</span>
}
}
const renderCommonProperties = () => (
<>
<PropertyRow label={translate('::App.Platform.ObjectType')} value={getObjectTypeName()} />
<PropertyRow label={translate('::App.Platform.ID')} value={object.id} />
{object.creationTime && (
<PropertyRow
label={translate('::App.Platform.Created')}
value={dayjs(object.creationTime).format('DD/MM/YYYY HH:mm')}
/>
)}
{object.lastModificationTime && (
<PropertyRow
label={translate('::App.Platform.Modified')}
value={dayjs(object.lastModificationTime).format('DD/MM/YYYY HH:mm')}
/>
)}
</>
)
const renderQueryProperties = () => {
const query = object as SqlQueryDto
return (
<>
<PropertyRow label={translate('::App.Platform.Code')} value={query.code} />
<PropertyRow label={translate('::App.Platform.Name')} value={query.name} />
<PropertyRow label={translate('::App.Platform.Description')} value={query.description} />
<PropertyRow label={translate('::App.Platform.DataSource')} value={query.dataSourceCode} />
<PropertyRow label={translate('::App.Platform.Status')} value={getStatusBadge(query.status)} />
<PropertyRow label={translate('::App.Platform.Category')} value={query.category} />
<PropertyRow label={translate('::App.Platform.Tags')} value={query.tags} />
<PropertyRow
label={translate('::App.Platform.ModifiesData')}
value={query.isModifyingData ? translate('::App.Platform.Yes') : translate('::App.Platform.No')}
/>
<PropertyRow label={translate('::App.Platform.ExecutionCount')} value={query.executionCount} />
{query.lastExecutedAt && (
<PropertyRow
label={translate('::App.Platform.LastExecuted')}
value={dayjs(query.lastExecutedAt).format('DD/MM/YYYY HH:mm')}
/>
)}
{renderCommonProperties()}
</>
)
}
const renderStoredProcedureProperties = () => {
const sp = object as SqlStoredProcedureDto
return (
<>
<PropertyRow label={translate('::App.Platform.ProcedureName')} value={sp.procedureName} />
<PropertyRow label={translate('::App.Platform.Schema')} value={sp.schemaName} />
<PropertyRow label={translate('::App.Platform.DisplayName')} value={sp.displayName} />
<PropertyRow label={translate('::App.Platform.Description')} value={sp.description} />
<PropertyRow label={translate('::App.Platform.DataSource')} value={sp.dataSourceCode} />
<PropertyRow label={translate('::App.Platform.Status')} value={getStatusBadge(sp.status)} />
<PropertyRow label={translate('::App.Platform.Category')} value={sp.category} />
<PropertyRow label={translate('::App.Platform.Deployed')} value={sp.isDeployed ? translate('::App.Platform.Yes') : translate('::App.Platform.No')} />
{sp.lastDeployedAt && (
<PropertyRow
label={translate('::App.Platform.LastDeployed')}
value={dayjs(sp.lastDeployedAt).format('DD/MM/YYYY HH:mm')}
/>
)}
{renderCommonProperties()}
</>
)
}
const renderViewProperties = () => {
const view = object as SqlViewDto
return (
<>
<PropertyRow label={translate('::App.Platform.ViewName')} value={view.viewName} />
<PropertyRow label={translate('::App.Platform.Schema')} value={view.schemaName} />
<PropertyRow label={translate('::App.Platform.DisplayName')} value={view.displayName} />
<PropertyRow label={translate('::App.Platform.Description')} value={view.description} />
<PropertyRow label={translate('::App.Platform.DataSource')} value={view.dataSourceCode} />
<PropertyRow label={translate('::App.Platform.Status')} value={getStatusBadge(view.status)} />
<PropertyRow label={translate('::App.Platform.Category')} value={view.category} />
<PropertyRow label={translate('::App.Platform.Deployed')} value={view.isDeployed ? translate('::App.Platform.Yes') : translate('::App.Platform.No')} />
<PropertyRow
label={translate('::App.Platform.SchemaBinding')}
value={view.withSchemaBinding ? translate('::App.Platform.Yes') : translate('::App.Platform.No')}
/>
{view.lastDeployedAt && (
<PropertyRow
label={translate('::App.Platform.LastDeployed')}
value={dayjs(view.lastDeployedAt).format('DD/MM/YYYY HH:mm')}
/>
)}
{renderCommonProperties()}
</>
)
}
const renderFunctionProperties = () => {
const func = object as SqlFunctionDto
const getFunctionType = (funcType: number) => {
switch (funcType) {
case 1:
return translate('::App.Platform.ScalarFunction')
case 2:
return translate('::App.Platform.TableValuedFunction')
case 3:
return translate('::App.Platform.InlineTableValuedFunction')
default:
return translate('::App.Platform.Unknown')
}
}
return (
<>
<PropertyRow label={translate('::App.Platform.FunctionName')} value={func.functionName} />
<PropertyRow label={translate('::App.Platform.Schema')} value={func.schemaName} />
<PropertyRow label={translate('::App.Platform.DisplayName')} value={func.displayName} />
<PropertyRow label={translate('::App.Platform.Description')} value={func.description} />
<PropertyRow label={translate('::App.Platform.FunctionType')} value={getFunctionType(func.functionType)} />
<PropertyRow label={translate('::App.Platform.ReturnType')} value={func.returnType} />
<PropertyRow label={translate('::App.Platform.DataSource')} value={func.dataSourceCode} />
<PropertyRow label={translate('::App.Platform.Status')} value={getStatusBadge(func.status)} />
<PropertyRow label={translate('::App.Platform.Category')} value={func.category} />
<PropertyRow label={translate('::App.Platform.Deployed')} value={func.isDeployed ? translate('::App.Platform.Yes') : translate('::App.Platform.No')} />
{func.lastDeployedAt && (
<PropertyRow
label={translate('::App.Platform.LastDeployed')}
value={dayjs(func.lastDeployedAt).format('DD/MM/YYYY HH:mm')}
/>
)}
{renderCommonProperties()}
</>
)
}
return (
<div className="h-full flex flex-col">
<div className="mb-4 pb-2 border-b">
<h6 className="font-bold">{translate('::App.Platform.Properties')}</h6>
</div>
<div className="flex-1 overflow-auto p-2">
{type === 1 && renderQueryProperties()}
{type === 2 && renderStoredProcedureProperties()}
{type === 3 && renderViewProperties()}
{type === 4 && renderFunctionProperties()}
</div>
</div>
)
}
export default SqlObjectProperties

View file

@ -0,0 +1,142 @@
import { useMemo } from 'react'
import { DataGrid } from 'devextreme-react'
import { Column, Paging, Scrolling, SearchPanel, Export, Selection } from 'devextreme-react/data-grid'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { HiOutlineCheckCircle, HiOutlineXCircle } from 'react-icons/hi'
interface SqlResultsGridProps {
result: SqlQueryExecutionResultDto
}
const SqlResultsGrid = ({ result }: SqlResultsGridProps) => {
const { translate } = useLocalization()
const columns = useMemo(() => {
// Get columns from metadata if available
if (result.metadata?.columns && Array.isArray(result.metadata.columns)) {
return result.metadata.columns
}
// Otherwise infer from data
if (result.data && result.data.length > 0) {
const firstRow = result.data[0]
return Object.keys(firstRow).map((key) => ({
name: key,
dataType: typeof firstRow[key],
isNullable: true,
}))
}
return []
}, [result])
const dataSource = useMemo(() => {
return result.data || []
}, [result])
if (!result.success) {
return (
<div className="h-full flex flex-col">
<div className="flex items-center gap-2 mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
<HiOutlineXCircle className="text-red-500 text-xl" />
<div>
<div className="font-semibold text-red-700 dark:text-red-400">
{translate('::App.Platform.Error')}
</div>
<div className="text-sm text-red-600 dark:text-red-500">
{result.error || result.message}
</div>
</div>
</div>
</div>
)
}
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-2 p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded">
<div className="flex items-center gap-2">
<HiOutlineCheckCircle className="text-green-500" />
<span className="text-sm text-green-700 dark:text-green-400">
{result.message || translate('::App.Platform.QueryExecutedSuccessfully')}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span>
{translate('::App.Platform.Rows')}: <strong>{result.rowsAffected || dataSource.length}</strong>
</span>
<span>
{translate('::App.Platform.Time')}: <strong>{result.executionTimeMs}ms</strong>
</span>
</div>
</div>
{dataSource.length > 0 ? (
<div className="flex-1 overflow-hidden relative">
<div className="absolute inset-0">
<DataGrid
dataSource={dataSource}
showBorders={true}
showRowLines={true}
showColumnLines={true}
rowAlternationEnabled={true}
columnAutoWidth={true}
wordWrapEnabled={false}
allowColumnReordering={true}
allowColumnResizing={true}
columnResizingMode="widget"
height="100%"
>
<Scrolling mode="virtual" rowRenderingMode="virtual" />
<Paging enabled={true} pageSize={50} />
<SearchPanel visible={true} width={240} placeholder={translate('::App.Platform.Search')} />
<Export enabled={true} allowExportSelectedData={true} />
<Selection mode="multiple" showCheckBoxesMode="always" />
{columns.map((col, index) => (
<Column
key={col.name || index}
dataField={col.name}
caption={col.name}
dataType={
col.dataType?.toLowerCase().includes('int') ||
col.dataType?.toLowerCase().includes('decimal') ||
col.dataType?.toLowerCase().includes('numeric') ||
col.dataType?.toLowerCase().includes('float') ||
col.dataType?.toLowerCase().includes('money')
? 'number'
: col.dataType?.toLowerCase().includes('date') ||
col.dataType?.toLowerCase().includes('time')
? 'date'
: col.dataType?.toLowerCase().includes('bit') ||
col.dataType?.toLowerCase().includes('boolean')
? 'boolean'
: 'string'
}
allowSorting={true}
allowFiltering={true}
allowHeaderFiltering={true}
/>
))}
</DataGrid>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500">
<div className="text-center">
<div className="text-lg mb-2">{translate('::App.Platform.NoResults')}</div>
<div className="text-sm">
{result.rowsAffected > 0
? translate('::App.Platform.RowCount', { count: result.rowsAffected })
: translate('::App.Platform.NoResultsReturned')}
</div>
</div>
</div>
)}
</div>
)
}
export default SqlResultsGrid