diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json index 98dd506f..01df8863 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Erp.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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" } ] -} \ No newline at end of file +} diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/MenusData.json b/api/src/Erp.Platform.DbMigrator/Seeds/MenusData.json index 35805f33..be403211 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/MenusData.json +++ b/api/src/Erp.Platform.DbMigrator/Seeds/MenusData.json @@ -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", diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/PermissionsData.json b/api/src/Erp.Platform.DbMigrator/Seeds/PermissionsData.json index 2d802df1..678c1ed4 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/PermissionsData.json +++ b/api/src/Erp.Platform.DbMigrator/Seeds/PermissionsData.json @@ -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", diff --git a/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205085044_Initial.Designer.cs b/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205115506_Initial.Designer.cs similarity index 99% rename from api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205085044_Initial.Designer.cs rename to api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205115506_Initial.Designer.cs index 3fae3871..14001e8a 100644 --- a/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205085044_Initial.Designer.cs +++ b/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205115506_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Erp.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20251205085044_Initial")] + [Migration("20251205115506_Initial")] partial class Initial { /// diff --git a/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205085044_Initial.cs b/api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205115506_Initial.cs similarity index 100% rename from api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205085044_Initial.cs rename to api/src/Erp.Platform.EntityFrameworkCore/Migrations/20251205115506_Initial.cs diff --git a/ui/src/proxy/sql-query-manager/models.ts b/ui/src/proxy/sql-query-manager/models.ts new file mode 100644 index 00000000..6754d0e3 --- /dev/null +++ b/ui/src/proxy/sql-query-manager/models.ts @@ -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 { + 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 { + 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 +} + +export interface ExecuteSavedQueryDto { + id: string + parameters?: Record +} + +export interface ValidateQueryDto { + queryText: string + dataSourceCode: string +} + +// SQL Stored Procedure DTOs +export interface SqlStoredProcedureDto extends FullAuditedEntityDto { + 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 { + 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 + 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 +} diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index 9658701b..1ad78f5a 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -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', }, } diff --git a/ui/src/proxy/data-source/data-source.service.ts b/ui/src/services/data-source.service.ts similarity index 93% rename from ui/src/proxy/data-source/data-source.service.ts rename to ui/src/services/data-source.service.ts index fd1bc51d..34afa546 100644 --- a/ui/src/proxy/data-source/data-source.service.ts +++ b/ui/src/services/data-source.service.ts @@ -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' diff --git a/ui/src/services/sql-query-manager.service.ts b/ui/src/services/sql-query-manager.service.ts new file mode 100644 index 00000000..6753deac --- /dev/null +++ b/ui/src/services/sql-query-manager.service.ts @@ -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) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-function', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + getList = (input: GetSqlFunctionsInput, config?: Partial) => + apiService.fetchData, GetSqlFunctionsInput>( + { + method: 'GET', + url: '/api/app/sql-function', + params: input, + }, + { apiName: this.apiName, ...config }, + ) + + get = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: `/api/app/sql-function/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + update = (id: string, input: UpdateSqlFunctionDto, config?: Partial) => + apiService.fetchData( + { + method: 'PUT', + url: `/api/app/sql-function/${id}`, + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + delete = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'DELETE', + url: `/api/app/sql-function/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + deploy = (input: DeployFunctionDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-function/deploy', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + checkExists = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-function/${id}/check-exists`, + }, + { apiName: this.apiName, ...config }, + ) + + drop = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-function/${id}/drop`, + }, + { apiName: this.apiName, ...config }, + ) +} + +export class SqlQueryService { + apiName = 'Default' + + create = (input: CreateSqlQueryDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-query', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + getList = (input: GetSqlQueriesInput, config?: Partial) => + apiService.fetchData, GetSqlQueriesInput>( + { + method: 'GET', + url: '/api/app/sql-query', + params: input, + }, + { apiName: this.apiName, ...config }, + ) + + get = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: `/api/app/sql-query/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + update = (id: string, input: UpdateSqlQueryDto, config?: Partial) => + apiService.fetchData( + { + method: 'PUT', + url: `/api/app/sql-query/${id}`, + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + delete = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'DELETE', + url: `/api/app/sql-query/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + executeQuery = (input: ExecuteSqlQueryDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-query/execute-query', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + executeSavedQuery = (id: string, parameters?: Record, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-query/${id}/execute-saved-query`, + data: { id, parameters }, + }, + { apiName: this.apiName, ...config }, + ) + + validateQuery = (input: ValidateQueryDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-query/validate-query', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + activate = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-query/${id}/activate`, + }, + { apiName: this.apiName, ...config }, + ) + + archive = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-query/${id}/archive`, + }, + { apiName: this.apiName, ...config }, + ) +} + +export class SqlStoredProcedureService { + apiName = 'Default' + + create = (input: CreateSqlStoredProcedureDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-stored-procedure', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + getList = (input: GetSqlStoredProceduresInput, config?: Partial) => + apiService.fetchData, GetSqlStoredProceduresInput>( + { + method: 'GET', + url: '/api/app/sql-stored-procedure', + params: input, + }, + { apiName: this.apiName, ...config }, + ) + + get = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: `/api/app/sql-stored-procedure/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + update = (id: string, input: UpdateSqlStoredProcedureDto, config?: Partial) => + apiService.fetchData( + { + method: 'PUT', + url: `/api/app/sql-stored-procedure/${id}`, + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + delete = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'DELETE', + url: `/api/app/sql-stored-procedure/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + deploy = (input: DeployStoredProcedureDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-stored-procedure/deploy', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + checkExists = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-stored-procedure/${id}/check-exists`, + }, + { apiName: this.apiName, ...config }, + ) + + drop = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-stored-procedure/${id}/drop`, + }, + { apiName: this.apiName, ...config }, + ) +} + +export class SqlViewService { + apiName = 'Default' + + create = (input: CreateSqlViewDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-view', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + getList = (input: GetSqlViewsInput, config?: Partial) => + apiService.fetchData, GetSqlViewsInput>( + { + method: 'GET', + url: '/api/app/sql-view', + params: input, + }, + { apiName: this.apiName, ...config }, + ) + + get = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: `/api/app/sql-view/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + update = (id: string, input: UpdateSqlViewDto, config?: Partial) => + apiService.fetchData( + { + method: 'PUT', + url: `/api/app/sql-view/${id}`, + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + delete = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'DELETE', + url: `/api/app/sql-view/${id}`, + }, + { apiName: this.apiName, ...config }, + ) + + deploy = (input: DeployViewDto, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/sql-view/deploy', + data: input, + }, + { apiName: this.apiName, ...config }, + ) + + checkExists = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-view/${id}/check-exists`, + }, + { apiName: this.apiName, ...config }, + ) + + drop = (id: string, config?: Partial) => + apiService.fetchData( + { + method: 'POST', + url: `/api/app/sql-view/${id}/drop`, + }, + { apiName: this.apiName, ...config }, + ) +} + +export class SqlTemplateService { + apiName = 'Default' + + getQueryTemplates = (config?: Partial) => + apiService.fetchData( + { + method: 'GET', + url: '/api/app/sql-template/query-templates', + }, + { apiName: this.apiName, ...config }, + ) + + getStoredProcedureTemplate = ( + procedureName: string, + schemaName = 'dbo', + config?: Partial, + ) => + apiService.fetchData( + { + 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, + ) => + apiService.fetchData( + { + 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, + ) => + apiService.fetchData( + { + method: 'GET', + url: '/api/app/sql-template/function-template', + params: { functionName, functionType, schemaName }, + }, + { apiName: this.apiName, ...config }, + ) + + getQueryTemplate = (templateType: string, config?: Partial) => + apiService.fetchData( + { + 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() diff --git a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx new file mode 100644 index 00000000..ca2614cb --- /dev/null +++ b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx @@ -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({ + 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( + + {translate('::App.Platform.FailedtoloadDatasources')} + , + { 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 = { + '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( + + {translate('::App.Platform.TemplateLoaded')} + , + { 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( + + {translate('::App.Platform.PleaseSelectDataSource')} + , + { placement: 'top-center' }, + ) + return + } + + if (!state.editorContent.trim()) { + toast.push( + + {translate('::App.Platform.PleaseEnterQuery')} + , + { 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( + + {translate('::App.Platform.QueryExecutedSuccessfully')} ({result.data.executionTimeMs}ms) + , + { placement: 'top-center' }, + ) + } catch (error: any) { + setState((prev) => ({ ...prev, isExecuting: false })) + toast.push( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToExecuteQuery')} + , + { placement: 'top-center' }, + ) + } + } + + const handleSave = async () => { + if (!state.selectedDataSource) { + toast.push( + + {translate('::App.Platform.PleaseSelectDataSource')} + , + { placement: 'top-center' }, + ) + return + } + + if (!state.editorContent.trim()) { + toast.push( + + {translate('::App.Platform.PleaseEnterContentToSave')} + , + { 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( + + {translate('::App.Platform.ObjectUpdatedSuccessfully')} + , + { placement: 'top-center' }, + ) + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToUpdateObject')} + , + { 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( + + {translate('::App.Platform.QuerySavedSuccessfully')} + , + { placement: 'top-center' }, + ) + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToSaveQuery')} + , + { placement: 'top-center' }, + ) + } + } + + const handleDeploy = async () => { + if (!state.selectedObject || !state.selectedObjectType) { + toast.push( + + {translate('::App.Platform.PleaseSelectAnObjectToDeploy')} + , + { 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( + + {translate('::App.Platform.ThisObjectTypeCannotBeDeployed')} + , + { placement: 'top-center' }, + ) + return + } + + toast.push( + + {translate('::App.Platform.ObjectDeployedSuccessfully')} + , + { placement: 'top-center' }, + ) + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToDeployObject')} + , + { placement: 'top-center' }, + ) + } + } + + return ( + +
+ {/* Toolbar */} + +
+
+ + + {translate('::App.Platform.DataSource')}: + + +
+ +
+ + + {state.selectedObject && + state.selectedObjectType && + state.selectedObjectType !== 1 && ( + + )} + +
+
+
+ + {/* Main Content Area */} +
+ {/* Left Panel - Object Explorer */} +
+ +
+
+
+ {translate('::App.Platform.ObjectExplorer')} +
+
+
+ +
+
+
+
+ + {/* Center Panel - Editor and Results */} +
+
+
+
{translate('::App.Platform.QueryEditor')}
+
+
+ +
+
+ + {state.executionResult && ( +
+
+
{translate('::App.Platform.Results')}
+
+
+ +
+
+ )} +
+ + {/* Right Panel - Properties (Optional) */} + {state.showProperties && state.selectedObject && ( +
+ +
+ )} +
+
+ + {/* Template Confirmation Dialog */} + +
{translate('::App.Platform.ConfirmTemplateReplace')}
+

+ {translate('::App.Platform.TemplateReplaceWarning')} +

+
+ + +
+
+ + {/* Save Dialog */} + setShowSaveDialog(false)} + onRequestClose={() => setShowSaveDialog(false)} + > +
{translate('::App.Platform.SaveAsNewQuery')}
+
+
+ + setSaveDialogData((prev) => ({ ...prev, name: e.target.value }))} + /> +
+
+ + + setSaveDialogData((prev) => ({ ...prev, description: e.target.value })) + } + /> +
+
+ + +
+
+
+
+ ) +} + +export default SqlQueryManager diff --git a/ui/src/views/sqlQueryManager/components/SqlEditor.tsx b/ui/src/views/sqlQueryManager/components/SqlEditor.tsx new file mode 100644 index 00000000..8ee943b5 --- /dev/null +++ b/ui/src/views/sqlQueryManager/components/SqlEditor.tsx @@ -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(null) + const monacoRef = useRef(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 ( +
+
+ +
+
+ ) +} + +export default SqlEditor diff --git a/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx new file mode 100644 index 00000000..63d24c4d --- /dev/null +++ b/ui/src/views/sqlQueryManager/components/SqlObjectExplorer.tsx @@ -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([]) + const [expandedNodes, setExpandedNodes] = useState>( + 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( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToLoadObjects')} + , + { 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( + + {translate('::App.Platform.ObjectDeletedSuccessfully')} + , + { placement: 'top-center' }, + ) + + setShowDeleteDialog(false) + setObjectToDelete(null) + loadObjects() + + if (selectedObject?.id === object.id) { + onObjectSelect(null, null) + } + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || translate('::App.Platform.FailedToDeleteObject')} + , + { placement: 'top-center' }, + ) + } + } + + const getIcon = (node: TreeNode) => { + if (node.type === 'root') return + + if (node.type === 'folder') { + const isExpanded = expandedNodes.has(node.id) + + // Templates folder + if (node.id === 'templates') + return isExpanded ? ( + + ) : ( + + ) + + if (node.objectType === 1) + return isExpanded ? ( + + ) : ( + + ) + + if (node.objectType === 2) + return isExpanded ? ( + + ) : ( + + ) + + if (node.objectType === 3) + return isExpanded ? ( + + ) : ( + + ) + + if (node.objectType === 4) + return isExpanded ? ( + + ) : ( + + ) + } + + if (node.type === 'object') { + // Check if it's a template + if ((node.data as any)?.templateType) { + return + } + + if (node.objectType === 1) return + if (node.objectType === 2) return + if (node.objectType === 3) return + if (node.objectType === 4) return + } + + return + } + + const renderNode = (node: TreeNode, level = 0) => { + const isExpanded = expandedNodes.has(node.id) + const isSelected = node.type === 'object' && selectedObject?.id === node.id + + return ( +
+
handleNodeClick(node)} + onContextMenu={(e) => handleContextMenu(e, node)} + > + {getIcon(node)} + {node.label} +
+ + {isExpanded && node.children && ( +
{node.children.map((child) => renderNode(child, level + 1))}
+ )} +
+ ) + } + + return ( +
+ {loading &&
{translate('::App.Platform.Loading')}
} + {!loading && treeData.length === 0 && ( +
+ {translate('::App.Platform.NoDataSourceSelected')} +
+ )} + {!loading && treeData.length > 0 && ( +
{treeData.map((node) => renderNode(node))}
+ )} + + {contextMenu.show && ( + <> +
setContextMenu({ show: false, x: 0, y: 0, node: null })} + /> +
+ {contextMenu.node?.type === 'object' && ( + <> + + + + + )} + + {contextMenu.node?.type === 'folder' && ( + + )} +
+ + )} + + setShowDeleteDialog(false)} + onRequestClose={() => setShowDeleteDialog(false)} + > +
{translate('::App.Platform.ConfirmDelete')}
+

{translate('::App.Platform.DeleteConfirmationMessage')}

+
+ + +
+
+
+ ) +} + +export default SqlObjectExplorer diff --git a/ui/src/views/sqlQueryManager/components/SqlObjectProperties.tsx b/ui/src/views/sqlQueryManager/components/SqlObjectProperties.tsx new file mode 100644 index 00000000..80bcae58 --- /dev/null +++ b/ui/src/views/sqlQueryManager/components/SqlObjectProperties.tsx @@ -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 ( +
+ {translate('::App.Platform.SelectAnObjectToViewProperties')} +
+ ) + } + + const PropertyRow = ({ label, value }: { label: string; value: any }) => ( +
+
{label}
+
{value || '-'}
+
+ ) + + 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 {translate('::App.Platform.Draft')} + case 2: + return {translate('::App.Platform.Active')} + case 3: + return {translate('::App.Platform.Archived')} + default: + return {translate('::App.Platform.Unknown')} + } + } + + const renderCommonProperties = () => ( + <> + + + {object.creationTime && ( + + )} + {object.lastModificationTime && ( + + )} + + ) + + const renderQueryProperties = () => { + const query = object as SqlQueryDto + return ( + <> + + + + + + + + + + {query.lastExecutedAt && ( + + )} + {renderCommonProperties()} + + ) + } + + const renderStoredProcedureProperties = () => { + const sp = object as SqlStoredProcedureDto + return ( + <> + + + + + + + + + {sp.lastDeployedAt && ( + + )} + {renderCommonProperties()} + + ) + } + + const renderViewProperties = () => { + const view = object as SqlViewDto + return ( + <> + + + + + + + + + + {view.lastDeployedAt && ( + + )} + {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 ( + <> + + + + + + + + + + + {func.lastDeployedAt && ( + + )} + {renderCommonProperties()} + + ) + } + + return ( +
+
+
{translate('::App.Platform.Properties')}
+
+ +
+ {type === 1 && renderQueryProperties()} + {type === 2 && renderStoredProcedureProperties()} + {type === 3 && renderViewProperties()} + {type === 4 && renderFunctionProperties()} +
+
+ ) +} + +export default SqlObjectProperties diff --git a/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx b/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx new file mode 100644 index 00000000..738ad4dc --- /dev/null +++ b/ui/src/views/sqlQueryManager/components/SqlResultsGrid.tsx @@ -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 ( +
+
+ +
+
+ {translate('::App.Platform.Error')} +
+
+ {result.error || result.message} +
+
+
+
+ ) + } + + return ( +
+ +
+
+ + + {result.message || translate('::App.Platform.QueryExecutedSuccessfully')} + +
+
+ + {translate('::App.Platform.Rows')}: {result.rowsAffected || dataSource.length} + + + {translate('::App.Platform.Time')}: {result.executionTimeMs}ms + +
+
+ + {dataSource.length > 0 ? ( +
+
+ + + + + + + + {columns.map((col, index) => ( + + ))} + +
+
+ ) : ( +
+
+
{translate('::App.Platform.NoResults')}
+
+ {result.rowsAffected > 0 + ? translate('::App.Platform.RowCount', { count: result.rowsAffected }) + : translate('::App.Platform.NoResultsReturned')} +
+
+
+ )} +
+ ) +} + +export default SqlResultsGrid