diff --git a/SQLVision.Core/Enums/ChartType.cs b/SQLVision.Core/Enums/ChartType.cs new file mode 100644 index 0000000..585a94a --- /dev/null +++ b/SQLVision.Core/Enums/ChartType.cs @@ -0,0 +1,12 @@ +namespace SQLVision.Core.Enums; + +public enum ChartType +{ + Line, + Bar, + Pie, + Area, + Scatter, + Heatmap, + Candlestick +} diff --git a/SQLVision.Core/Enums/DatabaseProvider.cs b/SQLVision.Core/Enums/DatabaseProvider.cs new file mode 100644 index 0000000..62c0cfe --- /dev/null +++ b/SQLVision.Core/Enums/DatabaseProvider.cs @@ -0,0 +1,10 @@ +namespace SQLVision.Core.Enums; + +public enum DatabaseProvider +{ + SqlServer, // Только MSSQL для начала + // PostgreSQL, + // MySQL, + // SQLite, + // Oracle +} diff --git a/SQLVision.Core/Enums/NotificationType.cs b/SQLVision.Core/Enums/NotificationType.cs new file mode 100644 index 0000000..3945c8e --- /dev/null +++ b/SQLVision.Core/Enums/NotificationType.cs @@ -0,0 +1,9 @@ +namespace SQLVision.Core.Enums; + +public enum NotificationType +{ + Information, + Success, + Warning, + Error +} \ No newline at end of file diff --git a/SQLVision.Core/Enums/OutputType.cs b/SQLVision.Core/Enums/OutputType.cs new file mode 100644 index 0000000..2f4186c --- /dev/null +++ b/SQLVision.Core/Enums/OutputType.cs @@ -0,0 +1,11 @@ +namespace SQLVision.Core.Enums; + +public enum OutputType +{ + Table, + Chart, + Text, + Grid, + Map, + Custom +} diff --git a/SQLVision.Core/Enums/ParameterType.cs b/SQLVision.Core/Enums/ParameterType.cs new file mode 100644 index 0000000..849b365 --- /dev/null +++ b/SQLVision.Core/Enums/ParameterType.cs @@ -0,0 +1,15 @@ +namespace SQLVision.Core.Enums; + +public enum ParameterType +{ + String, + Integer, + Decimal, + DateTime, + Boolean, + Table, // Для ComboBox с данными из БД + MultiSelect, // ListBox с множественным выбором + Color, + File, + Json +} \ No newline at end of file diff --git a/SQLVision.Core/Enums/ScriptChangeType.cs b/SQLVision.Core/Enums/ScriptChangeType.cs new file mode 100644 index 0000000..7af4758 --- /dev/null +++ b/SQLVision.Core/Enums/ScriptChangeType.cs @@ -0,0 +1,9 @@ +namespace SQLVision.Core.Enums; + +public enum ScriptChangeType +{ + Created, + Updated, + Deleted, + Renamed +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/IExportHandler.cs b/SQLVision.Core/Interfaces/IExportHandler.cs new file mode 100644 index 0000000..ce316cc --- /dev/null +++ b/SQLVision.Core/Interfaces/IExportHandler.cs @@ -0,0 +1,11 @@ +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Services.Exporters; + +public interface IExportHandler +{ + string FormatName { get; } + Task ExportAsync(DataTable data, string filePath, ExportOptions options); + Task ExportToMemoryAsync(DataTable data, ExportOptions options); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/IExportService.cs b/SQLVision.Core/Interfaces/IExportService.cs new file mode 100644 index 0000000..5699322 --- /dev/null +++ b/SQLVision.Core/Interfaces/IExportService.cs @@ -0,0 +1,11 @@ +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Core.Interfaces; + +public interface IExportService +{ + Task ExportAsync(DataTable data, string filePath, ExportOptions options); + Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options); + Task ExportToMemoryAsync(DataTable data, ExportOptions options); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/IMemoryExportHandler.cs b/SQLVision.Core/Interfaces/IMemoryExportHandler.cs new file mode 100644 index 0000000..d63266e --- /dev/null +++ b/SQLVision.Core/Interfaces/IMemoryExportHandler.cs @@ -0,0 +1,10 @@ +using SQLVision.Core.Models; +using System.Data; +using System.Threading.Tasks; + +namespace SQLVision.Core.Interfaces; + +public interface IMemoryExportHandler +{ + Task ExportToMemoryAsync(DataTable data, ExportOptions options); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/IPluginContext.cs b/SQLVision.Core/Interfaces/IPluginContext.cs new file mode 100644 index 0000000..38bda24 --- /dev/null +++ b/SQLVision.Core/Interfaces/IPluginContext.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SQLVision.Core.Enums; + +namespace SQLVision.Core.Interfaces; + +public interface IPluginContext +{ + IServiceProvider ServiceProvider { get; } + IConfiguration Configuration { get; } + ILogger Logger { get; } + Task ShowNotificationAsync(string message, NotificationType type); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/IPluginManager.cs b/SQLVision.Core/Interfaces/IPluginManager.cs new file mode 100644 index 0000000..9885c8d --- /dev/null +++ b/SQLVision.Core/Interfaces/IPluginManager.cs @@ -0,0 +1,13 @@ +using SQLVision.Core.Models; + +namespace SQLVision.Core.Interfaces; + +public interface IPluginManager +{ + void LoadPlugins(string pluginsDirectory); + IEnumerable GetPlugins(); + T? GetPlugin() where T : ISqlVisionPlugin; + + Task BeforeExecutionAsync(ScriptMetadata script, Dictionary parameters); + Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result); +} diff --git a/SQLVision.Core/Interfaces/IScriptManager.cs b/SQLVision.Core/Interfaces/IScriptManager.cs new file mode 100644 index 0000000..e18315e --- /dev/null +++ b/SQLVision.Core/Interfaces/IScriptManager.cs @@ -0,0 +1,38 @@ +using SQLVision.Core.Enums; +using SQLVision.Core.Models; + +namespace SQLVision.Core.Interfaces; + +public interface IScriptManager +{ + Task> LoadScriptsAsync(string? directory = null); + Task ReloadScriptAsync(string filePath); + void WatchDirectory(string directory, Action onScriptChanged); + + event EventHandler ScriptChanged; + event EventHandler ScriptsReloaded; +} + +public class ScriptChangedEventArgs : EventArgs +{ + public string FilePath { get; } + public ScriptChangeType ChangeType { get; } + public ScriptMetadata? Script { get; } + + public ScriptChangedEventArgs(string filePath, ScriptChangeType changeType, ScriptMetadata? script = null) + { + FilePath = filePath; + ChangeType = changeType; + Script = script; + } +} + +public class ScriptsReloadedEventArgs : EventArgs +{ + public IEnumerable Scripts { get; } + + public ScriptsReloadedEventArgs(IEnumerable scripts) + { + Scripts = scripts; + } +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/ISqlExecutionService.cs b/SQLVision.Core/Interfaces/ISqlExecutionService.cs new file mode 100644 index 0000000..57eb3ca --- /dev/null +++ b/SQLVision.Core/Interfaces/ISqlExecutionService.cs @@ -0,0 +1,30 @@ +using SQLVision.Core.Enums; +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Core.Interfaces; + +public interface ISqlExecutionService +{ + Task ExecuteAsync( + ScriptMetadata script, + Dictionary parameters, + CancellationToken cancellationToken = default); + + Task ExecuteAsync( + string sql, + Dictionary parameters, + string connectionString, + CancellationToken cancellationToken = default); + + Task TestConnectionAsync( + string connectionString, + DatabaseProvider provider, + CancellationToken cancellationToken = default); + + Task LoadComboBoxDataAsync( + string query, + string connectionString, + DatabaseProvider provider, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/ISqlScriptParser.cs b/SQLVision.Core/Interfaces/ISqlScriptParser.cs new file mode 100644 index 0000000..6d4fbe5 --- /dev/null +++ b/SQLVision.Core/Interfaces/ISqlScriptParser.cs @@ -0,0 +1,9 @@ +using SQLVision.Core.Models; + +namespace SQLVision.Core.Interfaces; + +public interface ISqlScriptParser +{ + ScriptMetadata Parse(string filePath, string sqlContent); + Task ParseAsync(string filePath, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/SQLVision.Core/Interfaces/ISqlVisionPlugin.cs b/SQLVision.Core/Interfaces/ISqlVisionPlugin.cs new file mode 100644 index 0000000..0705b63 --- /dev/null +++ b/SQLVision.Core/Interfaces/ISqlVisionPlugin.cs @@ -0,0 +1,11 @@ +namespace SQLVision.Core.Interfaces; + +public interface ISqlVisionPlugin +{ + string Name { get; } + string Description { get; } + Version Version { get; } + + Task InitializeAsync(IPluginContext context); + Task ShutdownAsync(); +} diff --git a/SQLVision.Core/Models/ChartSeries.cs b/SQLVision.Core/Models/ChartSeries.cs new file mode 100644 index 0000000..c8b24aa --- /dev/null +++ b/SQLVision.Core/Models/ChartSeries.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace SQLVision.Core.Models; + +public class ChartSeries +{ + public string Name { get; set; } + public List Values { get; set; } = new(); + public string Color { get; set; } + public double LineSmoothness { get; set; } = 0; + public bool ShowPoints { get; set; } = true; +} diff --git a/SQLVision.Core/Models/ExecutionHistoryItem.cs b/SQLVision.Core/Models/ExecutionHistoryItem.cs new file mode 100644 index 0000000..5e92145 --- /dev/null +++ b/SQLVision.Core/Models/ExecutionHistoryItem.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace SQLVision.Core.Models; + +public class ExecutionHistoryItem +{ + [JsonPropertyName("id")] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("scriptId")] + public string ScriptId { get; set; } = string.Empty; + + [JsonPropertyName("scriptName")] + public string ScriptName { get; set; } = string.Empty; + + [JsonPropertyName("executionTime")] + public DateTime ExecutionTime { get; set; } + + [JsonPropertyName("duration")] + public TimeSpan Duration { get; set; } + + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("parameters")] + public Dictionary Parameters { get; set; } = new(); + + [JsonPropertyName("rowCount")] + public int RowCount { get; set; } + + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + [JsonPropertyName("executedSql")] + public string? ExecutedSql { get; set; } +} \ No newline at end of file diff --git a/SQLVision.Core/Models/ExecutionResult.cs b/SQLVision.Core/Models/ExecutionResult.cs new file mode 100644 index 0000000..81646f5 --- /dev/null +++ b/SQLVision.Core/Models/ExecutionResult.cs @@ -0,0 +1,41 @@ +using System.Data; +using System.Text.Json.Serialization; + +namespace SQLVision.Core.Models; + +public class ExecutionResult +{ + [JsonPropertyName("data")] + public DataSet? Data { get; set; } + + [JsonPropertyName("isSuccess")] + public bool IsSuccess { get; set; } + + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + [JsonPropertyName("executionTime")] + public TimeSpan ExecutionTime { get; set; } + + [JsonPropertyName("isFromCache")] + public bool IsFromCache { get; set; } + + [JsonPropertyName("executionDate")] + public DateTime ExecutionDate { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("parameters")] + public Dictionary Parameters { get; set; } = new(); + + [JsonPropertyName("executedSql")] + public string ExecutedSql { get; set; } = string.Empty; + + [JsonPropertyName("rowCount")] + public int RowCount { get; set; } + + [JsonPropertyName("metrics")] + public Dictionary Metrics { get; set; } = new(); + + [JsonPropertyName("connectionName")] + public string? ConnectionName { get; set; } +} + diff --git a/SQLVision.Core/Models/ExportOptions.cs b/SQLVision.Core/Models/ExportOptions.cs new file mode 100644 index 0000000..267d90e --- /dev/null +++ b/SQLVision.Core/Models/ExportOptions.cs @@ -0,0 +1,12 @@ +namespace SQLVision.Core.Models; + +public class ExportOptions +{ + public string Format { get; set; } = "Excel"; + public bool IncludeHeaders { get; set; } = true; + public bool AutoFilter { get; set; } = true; + public bool IncludeCharts { get; set; } = false; + public string? ChartType { get; set; } + public bool OpenAfterExport { get; set; } = false; + public Dictionary CustomOptions { get; set; } = new(); +} \ No newline at end of file diff --git a/SQLVision.Core/Models/OutputDefinition.cs b/SQLVision.Core/Models/OutputDefinition.cs new file mode 100644 index 0000000..8e73790 --- /dev/null +++ b/SQLVision.Core/Models/OutputDefinition.cs @@ -0,0 +1,38 @@ +using SQLVision.Core.Enums; +using System.Text.Json.Serialization; + +namespace SQLVision.Core.Models; + +public class OutputDefinition +{ + [JsonPropertyName("type")] + public OutputType Type { get; set; } = OutputType.Table; + + [JsonPropertyName("subType")] + public string? SubType { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } = "Result"; + + [JsonPropertyName("isPrimary")] + public bool IsPrimary { get; set; } = false; + + [JsonPropertyName("options")] + public Dictionary Options { get; set; } = new(); + + [JsonPropertyName("dataTableName")] + public string? DataTableName { get; set; } + + // Для графиков + [JsonPropertyName("xAxisColumn")] + public string? XAxisColumn { get; set; } + + [JsonPropertyName("yAxisColumn")] + public string? YAxisColumn { get; set; } + + [JsonPropertyName("seriesColumn")] + public string? SeriesColumn { get; set; } + + [JsonPropertyName("chartType")] + public ChartType ChartType { get; set; } = ChartType.Line; +} \ No newline at end of file diff --git a/SQLVision.Core/Models/ScriptCategory.cs b/SQLVision.Core/Models/ScriptCategory.cs new file mode 100644 index 0000000..4d39b9a --- /dev/null +++ b/SQLVision.Core/Models/ScriptCategory.cs @@ -0,0 +1,8 @@ +namespace SQLVision.Core.Models; + +public class ScriptCategory +{ + public string Name { get; set; } = string.Empty; + public List Scripts { get; set; } = new(); + public bool IsExpanded { get; set; } = true; +} \ No newline at end of file diff --git a/SQLVision.Core/Models/ScriptMetadata.cs b/SQLVision.Core/Models/ScriptMetadata.cs new file mode 100644 index 0000000..34aba06 --- /dev/null +++ b/SQLVision.Core/Models/ScriptMetadata.cs @@ -0,0 +1,63 @@ +using SQLVision.Core.Enums; +using System.Text.Json.Serialization; + +namespace SQLVision.Core.Models; + +public class ScriptMetadata +{ + [JsonPropertyName("id")] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("fileName")] + public string FileName { get; set; } = string.Empty; + + [JsonPropertyName("fullPath")] + public string FullPath { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("rawSql")] + public string RawSql { get; set; } = string.Empty; + + [JsonPropertyName("processedSql")] + public string ProcessedSql { get; set; } = string.Empty; + + [JsonPropertyName("connectionString")] + public string? ConnectionString { get; set; } + + [JsonPropertyName("databaseProvider")] + public DatabaseProvider DatabaseProvider { get; set; } = DatabaseProvider.SqlServer; + + [JsonPropertyName("parameters")] + public List Parameters { get; set; } = new(); + + [JsonPropertyName("outputs")] + public List Outputs { get; set; } = new(); + + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } = new(); + + [JsonPropertyName("lastModified")] + public DateTime LastModified { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("category")] + public string? Category { get; set; } + + [JsonPropertyName("tags")] + public List Tags { get; set; } = new(); + + [JsonPropertyName("executionCount")] + public int ExecutionCount { get; set; } + + [JsonPropertyName("averageExecutionTime")] + public TimeSpan AverageExecutionTime { get; set; } + + [JsonIgnore] + public string DisplayName => !string.IsNullOrEmpty(Description) + ? Description + : Path.GetFileNameWithoutExtension(FileName); + + [JsonIgnore] + public bool IsVisible { get; set; } = true; +} diff --git a/SQLVision.Core/Models/ScriptParameter.cs b/SQLVision.Core/Models/ScriptParameter.cs new file mode 100644 index 0000000..5aa3697 --- /dev/null +++ b/SQLVision.Core/Models/ScriptParameter.cs @@ -0,0 +1,66 @@ +using SQLVision.Core.Enums; +using System.Text.Json.Serialization; + +namespace SQLVision.Core.Models; + +public class ScriptParameter +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public ParameterType Type { get; set; } = ParameterType.String; + + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("defaultValue")] + public object? DefaultValue { get; set; } + + [JsonPropertyName("isRequired")] + public bool IsRequired { get; set; } = false; + + [JsonPropertyName("order")] + public int Order { get; set; } = 0; + + [JsonPropertyName("group")] + public string? Group { get; set; } + + [JsonPropertyName("tableQuery")] + public string? TableQuery { get; set; } + + [JsonPropertyName("valueMember")] + public string ValueMember { get; set; } = "Id"; + + [JsonPropertyName("displayMember")] + public string DisplayMember { get; set; } = "Name"; + + [JsonPropertyName("dependsOn")] + public string? DependsOn { get; set; } + + [JsonPropertyName("dependencyValues")] + public Dictionary? DependencyValues { get; set; } + + [JsonPropertyName("validationRules")] + public Dictionary? ValidationRules { get; set; } + + [JsonPropertyName("watermark")] + public string? Watermark { get; set; } + + [JsonPropertyName("icon")] + public string? Icon { get; set; } + + [JsonIgnore] + public string EffectiveDisplayName => DisplayName ?? Name; + + public bool Validate(object? value) + { + if (IsRequired && (value == null || string.IsNullOrWhiteSpace(value.ToString()))) + return false; + + return true; + } +} \ No newline at end of file diff --git a/SQLVision.Core/SQLVision.Core.csproj b/SQLVision.Core/SQLVision.Core.csproj new file mode 100644 index 0000000..0d82d6a --- /dev/null +++ b/SQLVision.Core/SQLVision.Core.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.Services/Configuration/CacheOptions.cs b/SQLVision.Services/Configuration/CacheOptions.cs new file mode 100644 index 0000000..8b076c6 --- /dev/null +++ b/SQLVision.Services/Configuration/CacheOptions.cs @@ -0,0 +1,8 @@ +namespace SQLVision.Services.Configuration; + +public class CacheOptions +{ + public bool Enabled { get; set; } = true; + public int DurationMinutes { get; set; } = 10; + public long MaxSizeBytes { get; set; } = 100 * 1024 * 1024; // 100MB +} diff --git a/SQLVision.Services/Configuration/DatabaseOptions.cs b/SQLVision.Services/Configuration/DatabaseOptions.cs new file mode 100644 index 0000000..609b3b1 --- /dev/null +++ b/SQLVision.Services/Configuration/DatabaseOptions.cs @@ -0,0 +1,13 @@ +namespace SQLVision.Services.Configuration; + +public class DatabaseOptions +{ + public const string SectionName = "Database"; + + public string DefaultConnection { get; set; } = string.Empty; + public Dictionary ConnectionStrings { get; set; } = new(); + public int CommandTimeout { get; set; } = 300; + public bool EnableStatistics { get; set; } = true; + public CacheOptions Cache { get; set; } = new(); + public RetryOptions Retry { get; set; } = new(); +} diff --git a/SQLVision.Services/Configuration/RetryOptions.cs b/SQLVision.Services/Configuration/RetryOptions.cs new file mode 100644 index 0000000..939fdce --- /dev/null +++ b/SQLVision.Services/Configuration/RetryOptions.cs @@ -0,0 +1,8 @@ +namespace SQLVision.Services.Configuration; + +public class RetryOptions +{ + public bool Enabled { get; set; } = true; + public int MaxRetries { get; set; } = 3; + public int DelayMilliseconds { get; set; } = 1000; +} \ No newline at end of file diff --git a/SQLVision.Services/Exporters/CsvExporter.cs b/SQLVision.Services/Exporters/CsvExporter.cs new file mode 100644 index 0000000..fac3779 --- /dev/null +++ b/SQLVision.Services/Exporters/CsvExporter.cs @@ -0,0 +1,86 @@ +using CsvHelper; +using CsvHelper.Configuration; +using Microsoft.Extensions.Logging; +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Services.Exporters; + +public class CsvExporter : IExportHandler +{ + private readonly ILogger _logger; + + public string FormatName => "CSV"; + + public CsvExporter(ILogger logger) + { + _logger = logger; + } + + public async Task ExportAsync(DataTable data, string filePath, ExportOptions options) + { + var config = new CsvConfiguration(System.Globalization.CultureInfo.CurrentCulture) + { + Delimiter = options.CustomOptions.TryGetValue("Delimiter", out var delimiter) + ? delimiter.ToString() ?? "," + : "," + }; + + using var writer = new StreamWriter(filePath); + using var csv = new CsvWriter(writer, config); + + // Запись заголовков + if (options.IncludeHeaders) + { + foreach (DataColumn column in data.Columns) + { + csv.WriteField(column.ColumnName); + } + csv.NextRecord(); + } + + // Запись данных + foreach (DataRow row in data.Rows) + { + for (int i = 0; i < data.Columns.Count; i++) + { + csv.WriteField(row[i]); + } + csv.NextRecord(); + } + + await csv.FlushAsync(); + _logger.LogInformation("Exported {Rows} rows to CSV: {FilePath}", data.Rows.Count, filePath); + } + + public async Task ExportToMemoryAsync(DataTable data, ExportOptions options) + { + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture); + + // Запись заголовков + if (options.IncludeHeaders) + { + foreach (DataColumn column in data.Columns) + { + csv.WriteField(column.ColumnName); + } + csv.NextRecord(); + } + + // Запись данных + foreach (DataRow row in data.Rows) + { + for (int i = 0; i < data.Columns.Count; i++) + { + csv.WriteField(row[i]); + } + csv.NextRecord(); + } + + await csv.FlushAsync(); + await writer.FlushAsync(); + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/SQLVision.Services/Exporters/ExcelExporter.cs b/SQLVision.Services/Exporters/ExcelExporter.cs new file mode 100644 index 0000000..2883ba5 --- /dev/null +++ b/SQLVision.Services/Exporters/ExcelExporter.cs @@ -0,0 +1,139 @@ +using ClosedXML.Excel; +using Microsoft.Extensions.Logging; +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Services.Exporters; + +public class ExcelExporter : IExportHandler +{ + private readonly ILogger _logger; + + public string FormatName => "Excel"; + + public ExcelExporter(ILogger logger) + { + _logger = logger; + } + + public async Task ExportAsync(DataTable data, string filePath, ExportOptions options) + { + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Data"); + + WriteDataToWorksheet(worksheet, data, options); + + if (options.IncludeCharts && data.Rows.Count > 0) + { + AddCharts(worksheet, data, options); + } + + await Task.Run(() => workbook.SaveAs(filePath)); + _logger.LogInformation("Exported {Rows} rows to Excel: {FilePath}", data.Rows.Count, filePath); + } + + public async Task ExportToMemoryAsync(DataTable data, ExportOptions options) + { + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Data"); + + WriteDataToWorksheet(worksheet, data, options); + + using var stream = new MemoryStream(); + await Task.Run(() => workbook.SaveAs(stream)); + return stream.ToArray(); + } + + private void WriteDataToWorksheet(IXLWorksheet worksheet, DataTable data, ExportOptions options) + { + if (options.IncludeHeaders) + { + for (int col = 0; col < data.Columns.Count; col++) + { + worksheet.Cell(1, col + 1).Value = data.Columns[col].ColumnName; + worksheet.Cell(1, col + 1).Style.Font.Bold = true; + } + } + + int startRow = options.IncludeHeaders ? 2 : 1; + + for (int row = 0; row < data.Rows.Count; row++) + { + for (int col = 0; col < data.Columns.Count; col++) + { + var value = data.Rows[row][col]; + worksheet.Cell(startRow + row, col + 1).Value = ConvertValue(value); + + ApplyFormatting(worksheet.Cell(startRow + row, col + 1), value); + } + } + + if (options.AutoFilter) + { + var endRow = startRow + data.Rows.Count - 1; + worksheet.Range(1, 1, endRow, data.Columns.Count).SetAutoFilter(); + } + + worksheet.Columns().AdjustToContents(); + } + + private XLCellValue ConvertValue(object value) + { + if (value == null || value == DBNull.Value) return Blank.Value; + + if (value is DateTime dateTime) + return dateTime; + + if (value is decimal || value is double || value is float) + return Convert.ToDouble(value); + + if (value is bool b) + return b; + + return value.ToString(); + } + + private void ApplyFormatting(IXLCell cell, object value) + { + if (value is DateTime) + { + cell.Style.DateFormat.Format = "dd.MM.yyyy HH:mm:ss"; + } + else if (value is decimal || value is double || value is float) + { + cell.Style.NumberFormat.Format = "#,##0.00"; + } + } + + private void AddCharts(IXLWorksheet worksheet, DataTable data, ExportOptions options) + { + if (data.Columns.Count < 2) return; + + //TODO: chart + /*var chartType = GetChartType(options.ChartType); + var chart = worksheet.CreateChart(0, data.Columns.Count + 2, 20, data.Columns.Count + 10); + chart.ChartType = chartType; + + // Добавление серий на основе данных + for (int col = 1; col < Math.Min(5, data.Columns.Count); col++) + { + var series = chart.AddSeries( + worksheet.Range(2, col + 1, data.Rows.Count + 1, col + 1), + worksheet.Range(2, 1, data.Rows.Count + 1, 1)); + + series.ChartType = chartType; + } + */ + } + + private XLChartType GetChartType(string? chartType) => chartType?.ToLower() switch + { + "line" => XLChartType.Line, + "column" => XLChartType.ColumnClustered, + "bar" => XLChartType.BarClustered, + "pie" => XLChartType.Pie, + "area" => XLChartType.Area, + _ => XLChartType.Line + }; +} + diff --git a/SQLVision.Services/Exporters/JsonExporter.cs b/SQLVision.Services/Exporters/JsonExporter.cs new file mode 100644 index 0000000..3bc08d5 --- /dev/null +++ b/SQLVision.Services/Exporters/JsonExporter.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Logging; +using SQLVision.Core.Models; +using System.Data; +using System.Text.Json; + +namespace SQLVision.Services.Exporters; + +public class JsonExporter : IExportHandler +{ + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public string FormatName => "JSON"; + + public JsonExporter(ILogger logger) + { + _logger = logger; + _jsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + public async Task ExportAsync(DataTable data, string filePath, ExportOptions options) + { + var records = ConvertDataTableToList(data); + var json = JsonSerializer.Serialize(records, _jsonOptions); + await File.WriteAllTextAsync(filePath, json); + + _logger.LogInformation("Exported {Rows} rows to JSON: {FilePath}", data.Rows.Count, filePath); + } + + public async Task ExportToMemoryAsync(DataTable data, ExportOptions options) + { + var records = ConvertDataTableToList(data); + var json = JsonSerializer.Serialize(records, _jsonOptions); + return System.Text.Encoding.UTF8.GetBytes(json); + } + + private List> ConvertDataTableToList(DataTable data) + { + var list = new List>(); + + foreach (DataRow row in data.Rows) + { + var dict = new Dictionary(); + + foreach (DataColumn column in data.Columns) + { + dict[column.ColumnName] = row[column] ?? DBNull.Value; + } + + list.Add(dict); + } + + return list; + } +} \ No newline at end of file diff --git a/SQLVision.Services/Parsers/SqlScriptParser.cs b/SQLVision.Services/Parsers/SqlScriptParser.cs new file mode 100644 index 0000000..72807d8 --- /dev/null +++ b/SQLVision.Services/Parsers/SqlScriptParser.cs @@ -0,0 +1,312 @@ +using Microsoft.Extensions.Logging; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace SQLVision.Services.Parsers; + +public class SqlScriptParser : ISqlScriptParser +{ + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public SqlScriptParser(ILogger logger) + { + _logger = logger; + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + WriteIndented = true + }; + } + + public ScriptMetadata Parse(string filePath, string sqlContent) + { + var metadata = new ScriptMetadata + { + FileName = Path.GetFileName(filePath), + FullPath = filePath, + RawSql = sqlContent, + LastModified = File.GetLastWriteTimeUtc(filePath) + }; + + var lines = sqlContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var sqlBuilder = new StringBuilder(); + var inMultilineComment = false; + + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + + // Обработка многострочных комментариев + if (trimmedLine.StartsWith("/*")) + { + inMultilineComment = true; + + if (trimmedLine.Contains("*/")) + { + inMultilineComment = false; + ProcessInlineMultilineComment(trimmedLine, metadata); + } + else + { + ProcessMultilineCommentStart(trimmedLine, metadata); + } + continue; + } + + if (inMultilineComment) + { + if (trimmedLine.Contains("*/")) + { + inMultilineComment = false; + ProcessMultilineCommentEnd(trimmedLine, metadata); + } + else + { + ProcessMultilineCommentContent(trimmedLine, metadata); + } + continue; + } + + // Обработка однострочных комментариев + if (trimmedLine.StartsWith("--")) + { + ProcessSingleLineComment(trimmedLine, metadata); + } + else + { + sqlBuilder.AppendLine(line); + } + } + + metadata.ProcessedSql = sqlBuilder.ToString(); + ExtractCategoryAndTags(metadata); + + return metadata; + } + + private void ProcessSingleLineComment(string line, ScriptMetadata metadata) + { + // Удаляем "--" и триммируем + var content = line.Substring(2).Trim(); + + // Проверяем на директивы + if (content.StartsWith("@")) + { + ProcessDirective(content, metadata); + } + else if (string.IsNullOrEmpty(metadata.Description)) + { + // Первый комментарий без директивы - это описание + metadata.Description = content; + } + } + + private void ProcessDirective(string content, ScriptMetadata metadata) // Убрали ref + { + // Убираем "@" + content = content.Substring(1).Trim(); + + var spaceIndex = content.IndexOf(' '); + if (spaceIndex <= 0) return; + + var directive = content.Substring(0, spaceIndex).ToLower(); + var value = content.Substring(spaceIndex + 1).Trim(); + + try + { + switch (directive) + { + case "description": + metadata.Description = value.Trim('"'); + break; + + case "param": + var param = ParseParameter(value); + if (param != null) + metadata.Parameters.Add(param); + break; + + case "output": + var output = ParseOutput(value); + if (output != null) + metadata.Outputs.Add(output); + break; + + case "connection": + metadata.ConnectionString = value.Trim('"'); + break; + + case "database": + if (Enum.TryParse(value, true, out var provider)) + metadata.DatabaseProvider = provider; + break; + + case "category": + metadata.Category = value.Trim('"'); + break; + + case "tags": + metadata.Tags = value.Split(',') + .Select(t => t.Trim().Trim('"')) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList(); + break; + + case "metadata": + try + { + var metadataJson = JsonSerializer.Deserialize>( + value, _jsonOptions); + foreach (var kvp in metadataJson) + metadata.Metadata[kvp.Key] = kvp.Value; + } + catch { /* Игнорируем ошибки парсинга JSON */ } + break; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error parsing directive: {Directive}", directive); + } + } + + private ScriptParameter? ParseParameter(string value) + { + // Два формата: JSON и старый текстовый + if (value.TrimStart().StartsWith("{")) + { + return ParseJsonParameter(value); + } + else + { + return ParseLegacyParameter(value); + } + } + + private ScriptParameter? ParseJsonParameter(string json) + { + try + { + var param = JsonSerializer.Deserialize(json, _jsonOptions); + + // Валидация обязательных полей + if (string.IsNullOrEmpty(param?.Name)) + throw new ArgumentException("Parameter name is required"); + + if (param.Type == ParameterType.Table && string.IsNullOrEmpty(param.TableQuery)) + throw new ArgumentException("TableQuery is required for Table type"); + + return param; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error parsing JSON parameter"); + return null; + } + } + + private ScriptParameter? ParseLegacyParameter(string legacy) + { + // Формат: Type Name "Display Name" default="value" + var match = Regex.Match(legacy, + @"(\w+)\s+(\w+)\s+""([^""]+)""(?:\s+default=""([^""]*)"")?(?:\s+@table\s+""([^""]+)"")?"); + + if (!match.Success) return null; + + return new ScriptParameter + { + Type = Enum.TryParse(match.Groups[1].Value, true, out var type) + ? type : ParameterType.String, + Name = match.Groups[2].Value, + DisplayName = match.Groups[3].Value, + DefaultValue = match.Groups[4].Success ? match.Groups[4].Value : null, + TableQuery = match.Groups[5].Success ? match.Groups[5].Value : null + }; + } + + private OutputDefinition? ParseOutput(string value) + { + if (value.TrimStart().StartsWith("{")) + { + return JsonSerializer.Deserialize(value, _jsonOptions); + } + + // Старый формат: type:subtype "Description" + var match = Regex.Match(value, @"(\w+)(?::(\w+))?\s+""([^""]+)"""); + + if (!match.Success) return null; + + return new OutputDefinition + { + Type = Enum.TryParse(match.Groups[1].Value, true, out var type) + ? type : OutputType.Table, + SubType = match.Groups[2].Success ? match.Groups[2].Value : null, + Description = match.Groups[3].Value + }; + } + + private void ExtractCategoryAndTags(ScriptMetadata metadata) + { + // Извлекаем категорию из пути файла + if (string.IsNullOrEmpty(metadata.Category)) + { + var relativePath = Path.GetDirectoryName(metadata.FullPath); + if (!string.IsNullOrEmpty(relativePath)) + { + metadata.Category = Path.GetFileName(relativePath); + } + } + + // Автоматическое добавление тегов на основе имени файла + var fileName = Path.GetFileNameWithoutExtension(metadata.FileName); + var words = fileName.Split('_', '-', ' ') + .Where(w => w.Length > 2) + .Select(w => w.ToLower()); + + metadata.Tags.AddRange(words); + metadata.Tags = metadata.Tags.Distinct().ToList(); + } + + public async Task ParseAsync(string filePath, CancellationToken cancellationToken = default) + { + var content = await File.ReadAllTextAsync(filePath, cancellationToken); + return Parse(filePath, content); + } + + // Вспомогательные методы для многострочных комментариев + private void ProcessInlineMultilineComment(string line, ScriptMetadata metadata) + { + var content = line.Substring(2, line.IndexOf("*/") - 2).Trim(); + ProcessCommentContent(content, metadata); + } + + private void ProcessMultilineCommentStart(string line, ScriptMetadata metadata) + { + var content = line.Substring(2).Trim(); + ProcessCommentContent(content, metadata); + } + + private void ProcessMultilineCommentEnd(string line, ScriptMetadata metadata) + { + var content = line.Substring(0, line.IndexOf("*/")).Trim(); + ProcessCommentContent(content, metadata); + } + + private void ProcessMultilineCommentContent(string line, ScriptMetadata metadata) + { + ProcessCommentContent(line, metadata); + } + + private void ProcessCommentContent(string content, ScriptMetadata metadata) // Убрали ref + { + if (content.StartsWith("@")) + { + ProcessDirective(content, metadata); + } + } +} \ No newline at end of file diff --git a/SQLVision.Services/SQLVision.Services.csproj b/SQLVision.Services/SQLVision.Services.csproj new file mode 100644 index 0000000..afb4a0d --- /dev/null +++ b/SQLVision.Services/SQLVision.Services.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.Services/Services/ExportService.cs b/SQLVision.Services/Services/ExportService.cs new file mode 100644 index 0000000..24589d8 --- /dev/null +++ b/SQLVision.Services/Services/ExportService.cs @@ -0,0 +1,122 @@ +using Microsoft.Extensions.Logging; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using SQLVision.Services.Exporters; +using System.Data; + +namespace SQLVision.Services.Services; + +public class ExportService : IExportService +{ + private readonly ILogger _logger; + private readonly Dictionary _exportHandlers; + + public ExportService( + ILogger logger, + IEnumerable exportHandlers) + { + _logger = logger; + _exportHandlers = exportHandlers.ToDictionary(h => h.FormatName, h => h); + } + + public async Task ExportAsync(DataTable data, string filePath, ExportOptions options) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + var extension = Path.GetExtension(filePath).TrimStart('.').ToLower(); + var format = options?.Format ?? GetFormatFromExtension(extension); + + if (_exportHandlers.TryGetValue(format, out var handler)) + { + try + { + await handler.ExportAsync(data, filePath, options ?? new ExportOptions()); + _logger.LogInformation("Exported {Rows} rows to {FilePath} as {Format}", + data.Rows.Count, filePath, format); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting to {Format}", format); + throw; + } + } + else + { + throw new NotSupportedException($"Export format '{format}' is not supported"); + } + } + + public async Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options) + { + if (dataSet.Tables.Count == 0) + throw new InvalidOperationException("DataSet contains no tables"); + + if (dataSet.Tables.Count == 1) + { + await ExportAsync(dataSet.Tables[0], filePath, options); + } + else + { + // Для Excel создаем несколько листов + var extension = Path.GetExtension(filePath); + if (extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase)) + { + await ExportToMultiSheetExcel(dataSet, filePath, options); + } + else + { + // Для других форматов - экспортируем каждый лист в отдельный файл + var basePath = Path.Combine(Path.GetDirectoryName(filePath)!, + Path.GetFileNameWithoutExtension(filePath)); + + for (int i = 0; i < dataSet.Tables.Count; i++) + { + var table = dataSet.Tables[i]; + var tableFilePath = $"{basePath}_{table.TableName}_{i + 1}{extension}"; + await ExportAsync(table, tableFilePath, options); + } + } + } + } + + public async Task ExportToMemoryAsync(DataTable data, ExportOptions options) + { + var format = options?.Format ?? "Excel"; + + if (_exportHandlers.TryGetValue(format, out var handler)) + { + return await handler.ExportToMemoryAsync(data, options ?? new ExportOptions()); + } + + throw new NotSupportedException($"Export format '{format}' is not supported"); + } + + private async Task ExportToMultiSheetExcel(DataSet dataSet, string filePath, ExportOptions options) + { + using var workbook = new ClosedXML.Excel.XLWorkbook(); + + for (int i = 0; i < dataSet.Tables.Count; i++) + { + var table = dataSet.Tables[i]; + var worksheet = workbook.Worksheets.Add(table.TableName ?? $"Sheet{i + 1}"); + + // Используем ExcelExporter для записи данных + if (_exportHandlers.TryGetValue("Excel", out var excelHandler) && + excelHandler is ExcelExporter excelExporter) + { + var excelOptions = options ?? new ExportOptions(); + await excelExporter.ExportAsync(table, filePath, excelOptions); + } + } + + await Task.Run(() => workbook.SaveAs(filePath)); + } + + private string GetFormatFromExtension(string extension) => extension switch + { + "xlsx" => "Excel", + "csv" => "CSV", + "json" => "JSON", + _ => "Excel" + }; +} \ No newline at end of file diff --git a/SQLVision.Services/Services/PluginManager.cs b/SQLVision.Services/Services/PluginManager.cs new file mode 100644 index 0000000..52a3d2f --- /dev/null +++ b/SQLVision.Services/Services/PluginManager.cs @@ -0,0 +1,122 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System.Reflection; + +namespace SQLVision.Services.Services; + +public class PluginManager : IPluginManager +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly List _plugins = new(); + + public PluginManager(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public void LoadPlugins(string pluginsDirectory) + { + if (!Directory.Exists(pluginsDirectory)) + { + Directory.CreateDirectory(pluginsDirectory); + _logger.LogInformation("Created plugins directory: {Directory}", pluginsDirectory); + return; + } + + var pluginFiles = Directory.GetFiles(pluginsDirectory, "*.dll"); + _logger.LogInformation("Found {Count} plugin files", pluginFiles.Length); + + foreach (var pluginFile in pluginFiles) + { + try + { + var assembly = Assembly.LoadFrom(pluginFile); + var pluginTypes = assembly.GetTypes() + .Where(t => typeof(ISqlVisionPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + foreach (var pluginType in pluginTypes) + { + try + { + var plugin = (ISqlVisionPlugin)Activator.CreateInstance(pluginType)!; + var context = new PluginContext(_serviceProvider); + plugin.InitializeAsync(context).Wait(); + _plugins.Add(plugin); + _logger.LogInformation("Loaded plugin: {Name} v{Version}", plugin.Name, plugin.Version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize plugin from type {Type}", pluginType.FullName); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load plugin from {File}", pluginFile); + } + } + } + + public IEnumerable GetPlugins() => _plugins.AsReadOnly(); + + public T? GetPlugin() where T : ISqlVisionPlugin + => _plugins.OfType().FirstOrDefault(); + + public async Task BeforeExecutionAsync(ScriptMetadata script, Dictionary parameters) + { + foreach (var plugin in _plugins) + { + try + { + await plugin.InitializeAsync(new PluginContext(_serviceProvider)); + // TODO: Добавить метод BeforeExecution в ISqlVisionPlugin если нужно + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in plugin {Plugin} BeforeExecution", plugin.Name); + } + } + } + + public async Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result) + { + foreach (var plugin in _plugins) + { + try + { + // TODO: Добавить метод AfterExecution в ISqlVisionPlugin если нужно + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in plugin {Plugin} AfterExecution", plugin.Name); + } + } + } + + private class PluginContext : IPluginContext + { + public IServiceProvider ServiceProvider { get; } + public IConfiguration Configuration { get; } + public ILogger Logger { get; } + + public PluginContext(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Configuration = serviceProvider.GetRequiredService(); + Logger = serviceProvider.GetRequiredService>(); + } + + public Task ShowNotificationAsync(string message, NotificationType type) + { + // TODO: Реализовать показ уведомлений через UI + Logger.LogInformation("Plugin notification ({Type}): {Message}", type, message); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/SQLVision.Services/Services/ScriptManager.cs b/SQLVision.Services/Services/ScriptManager.cs new file mode 100644 index 0000000..535cbc9 --- /dev/null +++ b/SQLVision.Services/Services/ScriptManager.cs @@ -0,0 +1,197 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System.Collections.Concurrent; + +namespace SQLVision.Services.Services; + +public class ScriptManager : IScriptManager, IDisposable +{ + private readonly ISqlScriptParser _parser; + private readonly ILogger _logger; + private readonly FileSystemWatcher _watcher; + private readonly ConcurrentDictionary _scripts; + private readonly string _scriptsDirectory; + + public event EventHandler? ScriptChanged; + public event EventHandler? ScriptsReloaded; + + public ScriptManager(ISqlScriptParser parser, IConfiguration configuration, ILogger logger) + { + _parser = parser; + _logger = logger; + _scripts = new ConcurrentDictionary(); + _scriptsDirectory = configuration["Scripts:Directory"] ?? "Scripts"; + + _watcher = new FileSystemWatcher + { + Path = _scriptsDirectory, + Filter = "*.sql", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, + EnableRaisingEvents = false + }; + + _watcher.Changed += OnScriptChanged; + _watcher.Created += OnScriptCreated; + _watcher.Deleted += OnScriptDeleted; + _watcher.Renamed += OnScriptRenamed; + } + + public async Task> LoadScriptsAsync(string? directory = null) + { + var targetDirectory = directory ?? _scriptsDirectory; + + if (!Directory.Exists(targetDirectory)) + { + Directory.CreateDirectory(targetDirectory); + _logger.LogInformation("Created scripts directory: {Directory}", targetDirectory); + return Enumerable.Empty(); + } + + var sqlFiles = Directory.GetFiles(targetDirectory, "*.sql", SearchOption.AllDirectories); + var tasks = sqlFiles.Select(LoadScriptAsync); + var results = await Task.WhenAll(tasks); + + _scripts.Clear(); + foreach (var script in results.Where(s => s != null)) + { + _scripts[script!.FullPath] = script; + } + + StartWatching(); + + ScriptsReloaded?.Invoke(this, new ScriptsReloadedEventArgs(results.Where(s => s != null).ToList()!)); + + _logger.LogInformation("Loaded {Count} scripts from {Directory}", _scripts.Count, targetDirectory); + return _scripts.Values; + } + + private async Task LoadScriptAsync(string filePath) + { + try + { + return await _parser.ParseAsync(filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading script: {FilePath}", filePath); + return null; + } + } + + public async Task ReloadScriptAsync(string filePath) + { + try + { + var script = await LoadScriptAsync(filePath); + if (script != null) + { + _scripts[filePath] = script; + ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(filePath, ScriptChangeType.Updated, script)); + } + return script!; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reloading script: {FilePath}", filePath); + throw; + } + } + + private void OnScriptChanged(object sender, FileSystemEventArgs e) + { + // Задержка для избежания многократных вызовов + Task.Delay(300).ContinueWith(async _ => + { + try + { + var script = await ReloadScriptAsync(e.FullPath); + if (script != null) + { + _logger.LogInformation("Script changed: {FileName}", e.Name); + } + } + catch { /* Игнорируем ошибки */ } + }); + } + + private void OnScriptCreated(object sender, FileSystemEventArgs e) + { + Task.Delay(300).ContinueWith(async _ => + { + try + { + var script = await LoadScriptAsync(e.FullPath); + if (script != null) + { + _scripts[e.FullPath] = script; + ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Created, script)); + _logger.LogInformation("Script created: {FileName}", e.Name); + } + } + catch { /* Игнорируем */ } + }); + } + + private void OnScriptDeleted(object sender, FileSystemEventArgs e) + { + if (_scripts.TryRemove(e.FullPath, out var script)) + { + ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Deleted, script)); + _logger.LogInformation("Script deleted: {FileName}", e.Name); + } + } + + private void OnScriptRenamed(object sender, RenamedEventArgs e) + { + Task.Delay(300).ContinueWith(async _ => + { + try + { + // Удаляем старый файл + _scripts.TryRemove(e.OldFullPath, out var s); + + // Загружаем новый + var script = await LoadScriptAsync(e.FullPath); + if (script != null) + { + _scripts[e.FullPath] = script; + ScriptChanged?.Invoke(this, + new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Renamed, script)); + _logger.LogInformation("Script renamed: {OldName} -> {NewName}", + Path.GetFileName(e.OldFullPath), e.Name); + } + } + catch { /* Игнорируем */ } + }); + } + + private void StartWatching() + { + if (!_watcher.EnableRaisingEvents) + { + _watcher.EnableRaisingEvents = true; + _logger.LogDebug("Started watching directory: {Directory}", _scriptsDirectory); + } + } + + public void WatchDirectory(string directory, Action onScriptChanged) + { + if (_watcher.EnableRaisingEvents) + { + _watcher.EnableRaisingEvents = false; + } + + _watcher.Path = directory; + _watcher.EnableRaisingEvents = true; + + ScriptChanged += (sender, e) => onScriptChanged?.Invoke(e.FilePath); + } + + public void Dispose() + { + _watcher?.Dispose(); + } +} diff --git a/SQLVision.Services/Services/ServiceExtensions.cs b/SQLVision.Services/Services/ServiceExtensions.cs new file mode 100644 index 0000000..fd6aebd --- /dev/null +++ b/SQLVision.Services/Services/ServiceExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SQLVision.Core.Interfaces; +using SQLVision.Services.Exporters; +using SQLVision.Services.Parsers; +using SQLVision.Services.Services; + +namespace SQLVision.Services; + +public static class ServiceExtensions +{ + public static IServiceCollection AddSqlVisionServices(this IServiceCollection services) + { + // Регистрация парсера + services.TryAddSingleton(); + + // Регистрация сервиса выполнения SQL + services.TryAddSingleton(); + + // Регистрация менеджера скриптов + services.TryAddSingleton(); + + // Регистрация сервиса экспорта + services.TryAddSingleton(); + + // Регистрация менеджера плагинов + services.TryAddSingleton(); + + // Регистрация экспортеров + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/SQLVision.Services/Services/SqlExecutionService.cs b/SQLVision.Services/Services/SqlExecutionService.cs new file mode 100644 index 0000000..54c1e8b --- /dev/null +++ b/SQLVision.Services/Services/SqlExecutionService.cs @@ -0,0 +1,305 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using SQLVision.Services.Configuration; +using System.Collections.Concurrent; +using System.Data; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace SQLVision.Services.Services; + +public class SqlExecutionService : ISqlExecutionService, IDisposable +{ + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + private readonly IOptions _options; + private readonly ConcurrentDictionary> _loadingComboBoxData = new(); + + public SqlExecutionService( + IMemoryCache cache, + ILogger logger, + IOptions options) + { + _cache = cache; + _logger = logger; + _options = options; + } + + public async Task ExecuteAsync( + ScriptMetadata script, + Dictionary parameters, + CancellationToken cancellationToken = default) + { + var stopwatch = Stopwatch.StartNew(); + var result = new ExecutionResult + { + Parameters = new Dictionary(parameters), + ExecutionDate = DateTime.UtcNow, + ConnectionName = script.ConnectionString + }; + + try + { + // Генерация ключа кэша + var cacheKey = GenerateCacheKey(script, parameters); + if (_options.Value.Cache.Enabled) + { + if (_cache.TryGetValue(cacheKey, out var cachedResult)) + { + _logger.LogDebug("Returning cached result for {Script}", script.FileName); + cachedResult!.IsFromCache = true; + cachedResult.ExecutionTime = stopwatch.Elapsed; + return cachedResult; + } + } + + // Подготовка SQL с параметрами + var (processedSql, dbParameters) = PrepareSql( + script.ProcessedSql, + parameters, + script.DatabaseProvider); + + result.ExecutedSql = processedSql; + + // Выполнение запроса + var dataSet = await ExecuteQueryAsync( + processedSql, + dbParameters, + script.ConnectionString ?? _options.Value.DefaultConnection, + cancellationToken); + + stopwatch.Stop(); + + result.Data = dataSet; + result.IsSuccess = true; + result.ExecutionTime = stopwatch.Elapsed; + result.RowCount = dataSet.Tables.Cast().Sum(t => t.Rows.Count); + result.Metrics = new Dictionary + { + ["ExecutionTimeMs"] = stopwatch.ElapsedMilliseconds, + ["RowsAffected"] = result.RowCount, + ["TablesCount"] = dataSet.Tables.Count + }; + + // Кэширование результата + if (_options.Value.Cache.Enabled && result.RowCount > 0) + { + var cacheOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.Value.Cache.DurationMinutes), + Size = CalculateDataSetSize(dataSet) + }; + _cache.Set(cacheKey, result, cacheOptions); + } + + _logger.LogInformation( + "Executed {Script} in {ElapsedMs}ms, returned {Rows} rows", + script.FileName, stopwatch.ElapsedMilliseconds, result.RowCount); + + return result; + } + catch (Exception ex) + { + stopwatch.Stop(); + result.IsSuccess = false; + result.ErrorMessage = ex.Message; + result.ExecutionTime = stopwatch.Elapsed; + + _logger.LogError(ex, "Error executing script {Script}", script.FileName); + return result; + } + } + + private async Task ExecuteQueryAsync( + string sql, + List parameters, + string connectionString, + CancellationToken cancellationToken) + { + var dataSet = new DataSet(); + + await using var connection = new SqlConnection(connectionString); + await connection.OpenAsync(cancellationToken); + + await using var command = new SqlCommand(sql, connection); + command.CommandTimeout = _options.Value.CommandTimeout; + + // Добавление параметров + command.Parameters.AddRange(parameters.ToArray()); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + + do + { + var dataTable = new DataTable(); + dataTable.Load(reader); + dataSet.Tables.Add(dataTable); + } while (!reader.IsClosed && await reader.NextResultAsync(cancellationToken)); + + return dataSet; + } + + private (string Sql, List Parameters) PrepareSql( + string sql, + Dictionary parameters, + DatabaseProvider provider) + { + var dbParameters = new List(); + var processedSql = new StringBuilder(sql); + + foreach (var (key, value) in parameters) + { + var paramName = $"@{key}"; + var sqlParam = CreateSqlParameter(paramName, value); + dbParameters.Add(sqlParam); + } + + return (processedSql.ToString(), dbParameters); + } + + private SqlParameter CreateSqlParameter(string name, object? value) + { + var sqlParam = new SqlParameter(name, value ?? DBNull.Value); + + // Автоматическое определение типа данных + if (value is DateTime dateTime) + { + sqlParam.SqlDbType = SqlDbType.DateTime2; + sqlParam.Value = dateTime; + } + else if (value is int intValue) + { + sqlParam.SqlDbType = SqlDbType.Int; + sqlParam.Value = intValue; + } + else if (value is decimal decimalValue) + { + sqlParam.SqlDbType = SqlDbType.Decimal; + sqlParam.Value = decimalValue; + } + else if (value is bool boolValue) + { + sqlParam.SqlDbType = SqlDbType.Bit; + sqlParam.Value = boolValue; + } + else if (value is string stringValue) + { + sqlParam.SqlDbType = SqlDbType.NVarChar; + sqlParam.Value = stringValue; + sqlParam.Size = Math.Min(stringValue.Length * 2, 4000); // Ограничение для NVARCHAR + } + + return sqlParam; + } + + public async Task ExecuteAsync( + string sql, + Dictionary parameters, + string connectionString, + CancellationToken cancellationToken = default) + { + var script = new ScriptMetadata + { + ProcessedSql = sql, + ConnectionString = connectionString, + DatabaseProvider = DatabaseProvider.SqlServer + }; + + return await ExecuteAsync(script, parameters, cancellationToken); + } + + public async Task TestConnectionAsync( + string connectionString, + DatabaseProvider provider, + CancellationToken cancellationToken = default) + { + try + { + await using var connection = new SqlConnection(connectionString); + await connection.OpenAsync(cancellationToken); + await connection.CloseAsync(); + return true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Connection test failed for {Provider}", provider); + return false; + } + } + + public async Task LoadComboBoxDataAsync( + string query, + string connectionString, + DatabaseProvider provider, + CancellationToken cancellationToken = default) + { + var cacheKey = $"ComboBox_{provider}_{connectionString}_{query.GetHashCode()}"; + + return await _loadingComboBoxData.GetOrAdd(cacheKey, async key => + { + try + { + await using var connection = new SqlConnection(connectionString); + await using var command = new SqlCommand(query, connection); + command.CommandTimeout = 30; + + await connection.OpenAsync(cancellationToken); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + var dataTable = new DataTable(); + dataTable.Load(reader); + + return dataTable; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load combo box data for query: {Query}", query); + throw; + } + finally + { + _loadingComboBoxData.TryRemove(key, out _); + } + }); + } + + private string GenerateCacheKey(ScriptMetadata script, Dictionary parameters) + { + using var sha256 = SHA256.Create(); + + // Создаем строку для хэширования + var keyBuilder = new StringBuilder(); + keyBuilder.Append(script.Id); + + foreach (var param in parameters.OrderBy(p => p.Key)) + { + keyBuilder.Append($"|{param.Key}={param.Value}"); + } + + var keyData = keyBuilder.ToString(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData)); + return Convert.ToBase64String(hash); + } + + private long CalculateDataSetSize(DataSet dataSet) + { + long size = 0; + foreach (DataTable table in dataSet.Tables) + { + // Примерный расчет размера: кол-во строк * кол-во столбцов * средний размер + size += table.Rows.Count * table.Columns.Count * 64; // 64 байта на ячейку + } + return size; + } + + public void Dispose() + { + // Очищаем кэш загрузки данных для ComboBox + _loadingComboBoxData.Clear(); + } +} \ No newline at end of file diff --git a/SQLVision.UI/Controls/ControlFactory.cs b/SQLVision.UI/Controls/ControlFactory.cs new file mode 100644 index 0000000..9063885 --- /dev/null +++ b/SQLVision.UI/Controls/ControlFactory.cs @@ -0,0 +1,397 @@ +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace SQLVision.UI.Controls; + +public class ControlFactory : IControlFactory +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public ControlFactory(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + public FrameworkElement CreateControl(ScriptParameter parameter, Action onValueChanged) + { + try + { + var control = parameter.Type switch + { + ParameterType.String => CreateTextBox(parameter, onValueChanged), + ParameterType.Int => CreateNumberBox(parameter, onValueChanged), + ParameterType.Decimal => CreateNumberBox(parameter, onValueChanged, true), + ParameterType.DateTime => CreateDatePicker(parameter, onValueChanged), + ParameterType.Bool => CreateCheckBox(parameter, onValueChanged), + ParameterType.Table => CreateComboBox(parameter, onValueChanged), + ParameterType.MultiSelect => CreateListBox(parameter, onValueChanged), + ParameterType.Color => CreateColorPicker(parameter, onValueChanged), + ParameterType.File => CreateFilePicker(parameter, onValueChanged), + ParameterType.Json => CreateJsonEditor(parameter, onValueChanged), + _ => CreateTextBox(parameter, onValueChanged) + }; + + // Настройка общих свойств + control.Tag = parameter; + control.IsEnabled = !parameter.IsRequired || parameter.DefaultValue == null; + + // Добавление подсказки + if (!string.IsNullOrEmpty(parameter.Description)) + { + ToolTipService.SetToolTip(control, parameter.Description); + } + + return control; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating control for parameter {Parameter}", parameter.Name); + return CreateFallbackControl(parameter, onValueChanged); + } + } + + private FrameworkElement CreateTextBox(ScriptParameter parameter, Action onValueChanged) + { + var textBox = new TextBox + { + Header = parameter.DisplayName, + PlaceholderText = parameter.Watermark ?? $"Введите {parameter.DisplayName.ToLower()}", + Text = parameter.DefaultValue?.ToString() ?? string.Empty + }; + + textBox.TextChanged += (s, e) => onValueChanged(textBox.Text); + + return textBox; + } + + private FrameworkElement CreateNumberBox(ScriptParameter parameter, Action onValueChanged, bool isDecimal = false) + { + var numberBox = new NumberBox + { + Header = parameter.DisplayName, + PlaceholderText = parameter.Watermark ?? $"Введите {parameter.DisplayName.ToLower()}", + SmallChange = isDecimal ? 0.1 : 1, + LargeChange = isDecimal ? 1 : 10, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Inline, + AcceptsExpression = false + }; + + if (isDecimal) + { + numberBox.Value = Convert.ToDouble(parameter.DefaultValue ?? 0); + numberBox.ValueChanged += (s, e) => onValueChanged(e.NewValue); + } + else + { + numberBox.Value = Convert.ToInt32(parameter.DefaultValue ?? 0); + numberBox.ValueChanged += (s, e) => onValueChanged((int)e.NewValue); + } + + // Применение правил валидации + if (parameter.ValidationRules != null) + { + if (parameter.ValidationRules.TryGetValue("min", out var min)) + numberBox.Minimum = Convert.ToDouble(min); + + if (parameter.ValidationRules.TryGetValue("max", out var max)) + numberBox.Maximum = Convert.ToDouble(max); + } + + return numberBox; + } + + private FrameworkElement CreateDatePicker(ScriptParameter parameter, Action onValueChanged) + { + var datePicker = new DatePicker + { + Header = parameter.DisplayName, + Date = parameter.DefaultValue is DateTime defaultDate ? + DateTimeOffset.Parse(defaultDate.ToString()) : DateTimeOffset.Now + }; + + datePicker.DateChanged += (s, e) => onValueChanged(e.NewDate.DateTime); + + return datePicker; + } + + private FrameworkElement CreateCheckBox(ScriptParameter parameter, Action onValueChanged) + { + var checkBox = new CheckBox + { + Content = parameter.DisplayName, + IsChecked = parameter.DefaultValue is bool defaultBool ? defaultBool : false + }; + + checkBox.Checked += (s, e) => onValueChanged(true); + checkBox.Unchecked += (s, e) => onValueChanged(false); + + return checkBox; + } + + private FrameworkElement CreateComboBox(ScriptParameter parameter, Action onValueChanged) + { + var comboBox = new ComboBox + { + Header = parameter.DisplayName, + PlaceholderText = parameter.Watermark ?? $"Выберите {parameter.DisplayName.ToLower()}", + DisplayMemberPath = parameter.DisplayMember, + SelectedValuePath = parameter.ValueMember + }; + + // Загрузка данных асинхронно + LoadComboBoxDataAsync(comboBox, parameter).ConfigureAwait(false); + + comboBox.SelectionChanged += (s, e) => + { + if (comboBox.SelectedValue != null) + onValueChanged(comboBox.SelectedValue); + }; + + return comboBox; + } + + private async Task LoadComboBoxDataAsync(ComboBox comboBox, ScriptParameter parameter) + { + if (string.IsNullOrEmpty(parameter.TableQuery)) return; + + try + { + // Используем сервис выполнения SQL для загрузки данных + var executionService = _serviceProvider.GetService(); + var result = await executionService.ExecuteAsync( + parameter.TableQuery, + new Dictionary(), + GetConnectionString()); + + if (result.IsSuccess && result.Data.Tables.Count > 0) + { + comboBox.ItemsSource = result.Data.Tables[0].DefaultView; + + // Установка значения по умолчанию + if (parameter.DefaultValue != null) + { + comboBox.SelectedValue = parameter.DefaultValue; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading combo box data for {Parameter}", parameter.Name); + } + } + + private FrameworkElement CreateListBox(ScriptParameter parameter, Action onValueChanged) + { + var listBox = new ListBox + { + Header = parameter.DisplayName, + SelectionMode = ListViewSelectionMode.Multiple + }; + + // Загрузка данных для ListBox + // Аналогично ComboBox + + listBox.SelectionChanged += (s, e) => + { + var selectedValues = listBox.SelectedItems.Cast() + .Select(r => r[parameter.ValueMember]) + .ToList(); + + onValueChanged(selectedValues); + }; + + return listBox; + } + + private FrameworkElement CreateColorPicker(ScriptParameter parameter, Action onValueChanged) + { + var colorPicker = new ColorPicker + { + Header = parameter.DisplayName, + ColorSpectrumShape = ColorSpectrumShape.Box, + IsMoreButtonVisible = true, + IsColorSliderVisible = true, + IsColorChannelTextInputVisible = true, + IsHexInputVisible = true + }; + + if (parameter.DefaultValue is string defaultColor) + { + if (ColorHelper.TryParse(defaultColor, out var color)) + { + colorPicker.Color = color; + } + } + + colorPicker.ColorChanged += (s, e) => + onValueChanged($"#{e.NewColor.R:X2}{e.NewColor.G:X2}{e.NewColor.B:X2}"); + + return colorPicker; + } + + private FrameworkElement CreateFilePicker(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + + var textBox = new TextBox + { + Header = parameter.DisplayName, + PlaceholderText = "Путь к файлу", + Width = 200, + IsReadOnly = true + }; + + var button = new Button + { + Content = "Выбрать", + VerticalAlignment = VerticalAlignment.Bottom + }; + + button.Click += async (s, e) => + { + var openPicker = new FileOpenPicker(); + openPicker.ViewMode = PickerViewMode.List; + openPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + + if (parameter.ValidationRules != null && + parameter.ValidationRules.TryGetValue("extensions", out var extensions)) + { + foreach (var ext in extensions.ToString().Split(',')) + { + openPicker.FileTypeFilter.Add(ext.Trim()); + } + } + else + { + openPicker.FileTypeFilter.Add("*"); + } + + var file = await openPicker.PickSingleFileAsync(); + if (file != null) + { + textBox.Text = file.Path; + onValueChanged(file.Path); + } + }; + + stackPanel.Children.Add(textBox); + stackPanel.Children.Add(button); + + return stackPanel; + } + + private FrameworkElement CreateJsonEditor(ScriptParameter parameter, Action onValueChanged) + { + var textBox = new TextBox + { + Header = parameter.DisplayName, + PlaceholderText = "Введите JSON", + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + Height = 100, + FontFamily = new FontFamily("Consolas") + }; + + if (parameter.DefaultValue != null) + { + textBox.Text = JsonSerializer.Serialize(parameter.DefaultValue, + new JsonSerializerOptions { WriteIndented = true }); + } + + textBox.TextChanged += (s, e) => + { + try + { + var json = JsonSerializer.Deserialize(textBox.Text); + onValueChanged(json); + } + catch + { + // Игнорируем ошибки парсинга JSON + } + }; + + return textBox; + } + + private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action onValueChanged) + { + return new TextBox + { + Header = parameter.DisplayName, + Text = $"Ошибка создания контрола для типа {parameter.Type}", + IsReadOnly = true, + Foreground = new SolidColorBrush(Colors.Red) + }; + } + + public void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary currentValues) + { + // Обновление состояния контрола на основе зависимостей + if (!string.IsNullOrEmpty(parameter.DependsOn)) + { + var isEnabled = CheckDependency(parameter, currentValues); + control.IsEnabled = isEnabled; + + if (!isEnabled) + { + // Сброс значения, если контрол отключен + ResetControlValue(control); + } + } + } + + private bool CheckDependency(ScriptParameter parameter, Dictionary currentValues) + { + if (!currentValues.TryGetValue(parameter.DependsOn, out var dependencyValue)) + return false; + + if (parameter.DependencyValues != null) + { + return parameter.DependencyValues + .Any(kvp => object.Equals(kvp.Value, dependencyValue)); + } + + return dependencyValue != null && !string.IsNullOrWhiteSpace(dependencyValue.ToString()); + } + + private void ResetControlValue(FrameworkElement control) + { + switch (control) + { + case TextBox textBox: + textBox.Text = string.Empty; + break; + case ComboBox comboBox: + comboBox.SelectedIndex = -1; + break; + case CheckBox checkBox: + checkBox.IsChecked = false; + break; + case DatePicker datePicker: + datePicker.Date = DateTimeOffset.Now; + break; + } + } + + private string GetConnectionString() + { + // Получение строки подключения из конфигурации + var configuration = _serviceProvider.GetService(); + return configuration.GetConnectionString("Default") ?? + configuration["Database:DefaultConnection"]; + } +} \ No newline at end of file diff --git a/SQLVision.UI/SQLVision.UI.csproj b/SQLVision.UI/SQLVision.UI.csproj new file mode 100644 index 0000000..36d8ab8 --- /dev/null +++ b/SQLVision.UI/SQLVision.UI.csproj @@ -0,0 +1,33 @@ + + + + WinExe + net8.0-windows10.0.19041.0 + 10.0.17763.0 + SQLVision.UI + app.manifest + x86;x64;ARM64 + win10-x86;win10-x64;win10-arm64 + true + None + true + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.UI/ViewModels/MainViewModel.cs b/SQLVision.UI/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..be87b2e --- /dev/null +++ b/SQLVision.UI/ViewModels/MainViewModel.cs @@ -0,0 +1,528 @@ +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; + +namespace SQLVision.UI.ViewModels; + +public class MainViewModel : ObservableObject +{ + private readonly IScriptManager _scriptManager; + private readonly ISqlExecutionService _executionService; + private readonly IExportService _exportService; + private readonly IControlFactory _controlFactory; + private readonly IVisualizerFactory _visualizerFactory; + private readonly ILogger _logger; + + private readonly ObservableCollection _scripts = new(); + private readonly ObservableCollection _scriptCategories = new(); + private readonly ObservableCollection _history = new(); + private readonly ObservableCollection _resultTabs = new(); + + private ScriptMetadata _selectedScript; + private bool _isBusy; + private string _statusMessage; + private ResultTabViewModel _selectedResultTab; + private string _searchText; + + public ObservableCollection ScriptCategories => _scriptCategories; + public ObservableCollection History => _history; + public ObservableCollection ResultTabs => _resultTabs; + + public ScriptMetadata SelectedScript + { + get => _selectedScript; + set + { + if (SetProperty(ref _selectedScript, value)) + { + OnSelectedScriptChanged(); + } + } + } + + public bool IsBusy + { + get => _isBusy; + set => SetProperty(ref _isBusy, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public ResultTabViewModel SelectedResultTab + { + get => _selectedResultTab; + set => SetProperty(ref _selectedResultTab, value); + } + + public string SearchText + { + get => _searchText; + set + { + if (SetProperty(ref _searchText, value)) + { + FilterScripts(); + } + } + } + + public ObservableCollection Parameters { get; } = new(); + + public IAsyncRelayCommand LoadScriptsCommand { get; } + public IAsyncRelayCommand ExecuteCommand { get; } + public IAsyncRelayCommand ExportCommand { get; } + public IRelayCommand CopySqlCommand { get; } + public IRelayCommand ClearResultsCommand { get; } + public IRelayCommand SaveParametersCommand { get; } + public IRelayCommand LoadParametersCommand { get; } + + public MainViewModel( + IScriptManager scriptManager, + ISqlExecutionService executionService, + IExportService exportService, + IControlFactory controlFactory, + IVisualizerFactory visualizerFactory, + ILogger logger) + { + _scriptManager = scriptManager; + _executionService = executionService; + _exportService = exportService; + _controlFactory = controlFactory; + _visualizerFactory = visualizerFactory; + _logger = logger; + + LoadScriptsCommand = new AsyncRelayCommand(LoadScriptsAsync); + ExecuteCommand = new AsyncRelayCommand(ExecuteScriptAsync, CanExecuteScript); + ExportCommand = new AsyncRelayCommand(ExportResultsAsync, CanExportResults); + CopySqlCommand = new RelayCommand(CopySqlToClipboard); + ClearResultsCommand = new RelayCommand(ClearResults); + SaveParametersCommand = new RelayCommand(SaveParameters); + LoadParametersCommand = new RelayCommand(LoadParameters); + + // Подписка на события + _scriptManager.ScriptChanged += OnScriptChanged; + _scriptManager.ScriptsReloaded += OnScriptsReloaded; + } + + private async Task LoadScriptsAsync() + { + try + { + IsBusy = true; + StatusMessage = "Загрузка скриптов..."; + + var scripts = await _scriptManager.LoadScriptsAsync(); + _scripts.Clear(); + + foreach (var script in scripts) + { + _scripts.Add(script); + } + + CategorizeScripts(); + StatusMessage = $"Загружено {_scripts.Count} скриптов"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading scripts"); + StatusMessage = $"Ошибка загрузки: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } + + private void CategorizeScripts() + { + _scriptCategories.Clear(); + + var categories = _scripts + .GroupBy(s => s.Category ?? "Без категории") + .OrderBy(g => g.Key); + + foreach (var category in categories) + { + var scriptCategory = new ScriptCategory + { + Name = category.Key, + Scripts = new ObservableCollection(category.OrderBy(s => s.FileName)) + }; + + _scriptCategories.Add(scriptCategory); + } + } + + private void FilterScripts() + { + if (string.IsNullOrWhiteSpace(SearchText)) + { + // Показать все скрипты + foreach (var category in _scriptCategories) + { + foreach (var script in category.Scripts) + { + script.IsVisible = true; + } + } + } + else + { + var searchLower = SearchText.ToLower(); + + foreach (var category in _scriptCategories) + { + foreach (var script in category.Scripts) + { + script.IsVisible = + script.FileName.ToLower().Contains(searchLower) || + script.Description?.ToLower().Contains(searchLower) == true || + script.Tags.Any(t => t.ToLower().Contains(searchLower)); + } + } + } + } + + private void OnSelectedScriptChanged() + { + Parameters.Clear(); + + if (_selectedScript == null) return; + + // Создание ViewModel для каждого параметра + foreach (var param in _selectedScript.Parameters.OrderBy(p => p.Order)) + { + var paramVm = new ParameterViewModel(param, _controlFactory); + paramVm.ValueChanged += OnParameterValueChanged; + Parameters.Add(paramVm); + } + + // Восстановление сохраненных значений + LoadSavedParameters(); + } + + private void OnParameterValueChanged(object sender, EventArgs e) + { + ExecuteCommand.NotifyCanExecuteChanged(); + + // Обновление зависимых параметров + if (sender is ParameterViewModel changedParam) + { + UpdateDependentParameters(changedParam); + } + } + + private void UpdateDependentParameters(ParameterViewModel changedParam) + { + foreach (var paramVm in Parameters) + { + if (paramVm.Parameter.DependsOn == changedParam.Parameter.Name) + { + paramVm.UpdateDependencies(GetCurrentParameterValues()); + } + } + } + + private Dictionary GetCurrentParameterValues() + { + return Parameters.ToDictionary( + p => p.Parameter.Name, + p => p.Value ?? p.Parameter.DefaultValue); + } + + private bool CanExecuteScript() + { + if (_selectedScript == null) return false; + + // Проверка обязательных параметров + foreach (var paramVm in Parameters) + { + if (paramVm.Parameter.IsRequired && + (paramVm.Value == null || string.IsNullOrWhiteSpace(paramVm.Value.ToString()))) + { + return false; + } + } + + return true; + } + + private async Task ExecuteScriptAsync() + { + try + { + IsBusy = true; + StatusMessage = "Выполнение скрипта..."; + + var parameters = GetCurrentParameterValues(); + + var result = await _executionService.ExecuteAsync(_selectedScript, parameters); + + // Добавление в историю + var historyItem = new ExecutionHistoryItem + { + ScriptId = _selectedScript.Id, + ScriptName = _selectedScript.FileName, + ExecutionTime = DateTime.Now, + Duration = result.ExecutionTime, + Success = result.IsSuccess, + Parameters = new Dictionary(parameters), + RowCount = result.RowCount, + ErrorMessage = result.ErrorMessage, + ExecutedSql = result.ExecutedSql + }; + + _history.Insert(0, historyItem); + + // Очистка старых записей истории + while (_history.Count > 1000) + { + _history.RemoveAt(_history.Count - 1); + } + + if (result.IsSuccess) + { + CreateResultTabs(result); + StatusMessage = $"Выполнено за {result.ExecutionTime.TotalSeconds:F2} сек. Получено строк: {result.RowCount}"; + } + else + { + StatusMessage = $"Ошибка: {result.ErrorMessage}"; + ShowErrorDialog(result.ErrorMessage); + } + + // Сохранение параметров + SaveParameters(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing script"); + StatusMessage = $"Ошибка: {ex.Message}"; + ShowErrorDialog(ex.Message); + } + finally + { + IsBusy = false; + } + } + + private void CreateResultTabs(ExecutionResult result) + { + // Удаляем старые вкладки + ResultTabs.Clear(); + + for (int i = 0; i < result.Data.Tables.Count; i++) + { + var table = result.Data.Tables[i]; + var outputDef = i < _selectedScript.Outputs.Count + ? _selectedScript.Outputs[i] + : CreateDefaultOutputDefinition(table, i); + + var visualizer = _visualizerFactory.GetVisualizer(outputDef.Type); + var content = visualizer.Visualize(table, outputDef); + + var tabVm = new ResultTabViewModel + { + Title = outputDef.Description, + Content = content, + DataTable = table, + OutputDefinition = outputDef, + CanExport = true, + CanCopy = true + }; + + ResultTabs.Add(tabVm); + } + + if (ResultTabs.Any()) + { + SelectedResultTab = ResultTabs[0]; + } + } + + private OutputDefinition CreateDefaultOutputDefinition(DataTable table, int index) + { + return new OutputDefinition + { + Type = OutputType.Table, + Description = $"Результат {index + 1}", + DataTableName = table.TableName + }; + } + + private bool CanExportResults() + { + return SelectedResultTab != null && + SelectedResultTab.DataTable != null && + SelectedResultTab.DataTable.Rows.Count > 0; + } + + private async Task ExportResultsAsync() + { + if (SelectedResultTab?.DataTable == null) return; + + try + { + var savePicker = new FileSavePicker(); + savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + savePicker.FileTypeChoices.Add("Excel файл", new List { ".xlsx" }); + savePicker.FileTypeChoices.Add("CSV файл", new List { ".csv" }); + savePicker.FileTypeChoices.Add("JSON файл", new List { ".json" }); + savePicker.SuggestedFileName = $"{_selectedScript.FileName}_{DateTime.Now:yyyyMMdd_HHmmss}"; + + var file = await savePicker.PickSaveFileAsync(); + if (file != null) + { + var options = new ExportOptions + { + Format = Path.GetExtension(file.Path).TrimStart('.').ToUpper(), + IncludeHeaders = true, + AutoFilter = true + }; + + await _exportService.ExportAsync(SelectedResultTab.DataTable, file.Path, options); + + StatusMessage = $"Экспортировано в {file.Path}"; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error exporting results"); + StatusMessage = $"Ошибка экспорта: {ex.Message}"; + } + } + + private void CopySqlToClipboard() + { + if (_selectedScript == null) return; + + var parameters = GetCurrentParameterValues(); + var sql = FormatSqlWithParameters(_selectedScript.ProcessedSql, parameters); + + var package = new DataPackage(); + package.SetText(sql); + Clipboard.SetContent(package); + + StatusMessage = "SQL скопирован в буфер обмена"; + } + + private string FormatSqlWithParameters(string sql, Dictionary parameters) + { + var result = new StringBuilder(sql); + + foreach (var param in parameters) + { + var paramName = $"@{param.Key}"; + var paramValue = FormatParameterForDisplay(param.Value); + + result = result.Replace(paramName, paramValue); + } + + return result.ToString(); + } + + private string FormatParameterForDisplay(object value) + { + if (value == null) return "NULL"; + + return value switch + { + string str => $"N'{str.Replace("'", "''")}'", + DateTime dt => $"'{dt:yyyy-MM-dd HH:mm:ss}'", + bool b => b ? "1" : "0", + _ => value.ToString() + }; + } + + private void ClearResults() + { + ResultTabs.Clear(); + StatusMessage = "Результаты очищены"; + } + + private void SaveParameters() + { + if (_selectedScript == null) return; + + var parameters = GetCurrentParameterValues(); + var settings = ApplicationData.Current.LocalSettings; + + var dict = new Dictionary(); + foreach (var param in parameters) + { + dict[param.Key] = param.Value; + } + + var json = JsonSerializer.Serialize(dict); + settings.Values[$"ScriptParams_{_selectedScript.Id}"] = json; + + StatusMessage = "Параметры сохранены"; + } + + private void LoadParameters() + { + if (_selectedScript == null) return; + + var settings = ApplicationData.Current.LocalSettings; + if (settings.Values.TryGetValue($"ScriptParams_{_selectedScript.Id}", out var jsonObj)) + { + try + { + var savedParams = JsonSerializer.Deserialize>(jsonObj.ToString()); + + foreach (var paramVm in Parameters) + { + if (savedParams.TryGetValue(paramVm.Parameter.Name, out var value)) + { + paramVm.Value = value; + } + } + + StatusMessage = "Параметры загружены"; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error loading saved parameters"); + } + } + } + + private void ShowErrorDialog(string message) + { + // Реализация диалога ошибки + // Можно использовать ContentDialog или другое UI решение + } + + private void OnScriptChanged(object sender, ScriptChangedEventArgs e) + { + // Обновление UI при изменении скрипта + DispatcherQueue.GetForCurrentThread().TryEnqueue(() => + { + CategorizeScripts(); + + if (e.ChangeType == ScriptChangeType.Deleted && + _selectedScript?.FullPath == e.FilePath) + { + SelectedScript = null; + } + }); + } + + private void OnScriptsReloaded(object sender, ScriptsReloadedEventArgs e) + { + DispatcherQueue.GetForCurrentThread().TryEnqueue(CategorizeScripts); + } +} diff --git a/SQLVision.UI/ViewModels/ParameterViewModel.cs b/SQLVision.UI/ViewModels/ParameterViewModel.cs new file mode 100644 index 0000000..b2898e1 --- /dev/null +++ b/SQLVision.UI/ViewModels/ParameterViewModel.cs @@ -0,0 +1,124 @@ +using Microsoft.UI.Xaml; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SQLVision.UI.ViewModels; + +public class ParameterViewModel : ObservableObject +{ + private readonly IControlFactory _controlFactory; + private FrameworkElement _control; + private object _value; + private bool _isEnabled = true; + private string _validationError; + + public ScriptParameter Parameter { get; } + public string Name => Parameter.Name; + public string DisplayName => Parameter.DisplayName ?? Parameter.Name; + public string Description => Parameter.Description; + public ParameterType Type => Parameter.Type; + + public FrameworkElement Control + { + get + { + if (_control == null) + { + _control = _controlFactory.CreateControl(Parameter, OnValueChanged); + } + return _control; + } + } + + public object Value + { + get => _value ?? Parameter.DefaultValue; + set + { + if (SetProperty(ref _value, value)) + { + ValidateValue(); + ValueChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public bool IsEnabled + { + get => _isEnabled; + set => SetProperty(ref _isEnabled, value); + } + + public string ValidationError + { + get => _validationError; + private set => SetProperty(ref _validationError, value); + } + + public bool HasError => !string.IsNullOrEmpty(ValidationError); + + public event EventHandler ValueChanged; + + public ParameterViewModel(ScriptParameter parameter, IControlFactory controlFactory) + { + Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); + _controlFactory = controlFactory ?? throw new ArgumentNullException(nameof(controlFactory)); + + _value = parameter.DefaultValue; + } + + private void OnValueChanged(object value) + { + Value = value; + } + + private void ValidateValue() + { + if (Parameter.IsValid(Value)) + { + ValidationError = null; + } + else + { + ValidationError = "Неверное значение параметра"; + } + } + + public void UpdateDependencies(Dictionary currentValues) + { + if (string.IsNullOrEmpty(Parameter.DependsOn)) + { + IsEnabled = true; + return; + } + + if (!currentValues.TryGetValue(Parameter.DependsOn, out var dependencyValue)) + { + IsEnabled = false; + return; + } + + // Проверка условий зависимости + if (Parameter.DependencyValues != null) + { + var isEnabled = Parameter.DependencyValues + .Any(kvp => object.Equals(kvp.Value, dependencyValue)); + + IsEnabled = isEnabled; + } + else + { + IsEnabled = dependencyValue != null && + !string.IsNullOrWhiteSpace(dependencyValue.ToString()); + } + + if (!IsEnabled) + { + Value = null; + } + } +} diff --git a/SQLVision.UI/ViewModels/ResultTabViewModel.cs b/SQLVision.UI/ViewModels/ResultTabViewModel.cs new file mode 100644 index 0000000..e2b660b --- /dev/null +++ b/SQLVision.UI/ViewModels/ResultTabViewModel.cs @@ -0,0 +1,70 @@ +using Microsoft.UI.Xaml; +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.UI.ViewModels; + +public class ResultTabViewModel : ObservableObject +{ + private string _title; + private FrameworkElement _content; + private DataTable _dataTable; + private OutputDefinition _outputDefinition; + private bool _canExport; + private bool _canCopy; + + public string Title + { + get => _title; + set => SetProperty(ref _title, value); + } + + public FrameworkElement Content + { + get => _content; + set => SetProperty(ref _content, value); + } + + public DataTable DataTable + { + get => _dataTable; + set => SetProperty(ref _dataTable, value); + } + + public OutputDefinition OutputDefinition + { + get => _outputDefinition; + set => SetProperty(ref _outputDefinition, value); + } + + public bool CanExport + { + get => _canExport; + set => SetProperty(ref _canExport, value); + } + + public bool CanCopy + { + get => _canCopy; + set => SetProperty(ref _canCopy, value); + } + + public IRelayCommand ExportCommand { get; } + public IRelayCommand CopyDataCommand { get; } + + public ResultTabViewModel() + { + ExportCommand = new RelayCommand(Export); + CopyDataCommand = new RelayCommand(CopyData); + } + + private void Export() + { + // Экспорт данных вкладки + } + + private void CopyData() + { + // Копирование данных в буфер обмена + } +} diff --git a/SQLVision.UI/ViewModels/ScriptCategory.cs b/SQLVision.UI/ViewModels/ScriptCategory.cs new file mode 100644 index 0000000..69cab52 --- /dev/null +++ b/SQLVision.UI/ViewModels/ScriptCategory.cs @@ -0,0 +1,29 @@ +using SQLVision.Core.Models; +using System.Collections.ObjectModel; + +namespace SQLVision.UI.ViewModels; + +public class ScriptCategory : ObservableObject +{ + private string _name; + private ObservableCollection _scripts; + private bool _isExpanded = true; + + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public ObservableCollection Scripts + { + get => _scripts; + set => SetProperty(ref _scripts, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } +} \ No newline at end of file diff --git a/SQLVision.UI/Views/MainWindow.xaml b/SQLVision.UI/Views/MainWindow.xaml new file mode 100644 index 0000000..b522938 --- /dev/null +++ b/SQLVision.UI/Views/MainWindow.xaml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.UI/Views/MainWindow.xaml.cs b/SQLVision.UI/Views/MainWindow.xaml.cs new file mode 100644 index 0000000..2a6dffd --- /dev/null +++ b/SQLVision.UI/Views/MainWindow.xaml.cs @@ -0,0 +1,169 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using SQLVision.Core.Models; +using SQLVision.Services; +using System; +using System.Collections.Generic; +using System.IO; +using Windows.Graphics; + +namespace SQLVision.UI.Views +{ + public sealed partial class MainWindow : Window + { + // + private ScriptMetadata currentMetadata; + + public MainWindow() + { + this.InitializeComponent(); + SetWindowSize(1200, 800); + LoadScripts(); + } + + private void SetWindowSize(int width, int height) + { + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd); + var appWindow = AppWindow.GetFromWindowId(windowId); + appWindow.Resize(new SizeInt32(width, height)); + } + + private void LoadScripts() + { + var folder = Path.Combine(AppContext.BaseDirectory, "Scripts"); + if (!Directory.Exists(folder)) return; + + foreach (var file in Directory.GetFiles(folder, "*.sql", SearchOption.AllDirectories)) + { + var node = new TreeViewNode + { + Content = new ScriptTreeItem + { + DisplayName = Path.GetFileName(file), + FilePath = file + } + }; + ScriptsTree.RootNodes.Add(node); + } + + ScriptsTree.ItemInvoked += ScriptsTree_ItemInvoked; + } + + private void ScriptsTree_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args) + { + if (args.InvokedItem is TreeViewNode node && node.Content is ScriptTreeItem item) + { + var file = item.FilePath; + var lines = File.ReadAllLines(file); + var parser = new SqlScriptParser(); + currentMetadata = parser.Parse(lines); // + + RenderParameters(currentMetadata); + + OutputTabs.TabItems.Clear(); + foreach (var output in currentMetadata.Outputs) + { + var tab = new TabViewItem + { + Header = output.Description, + Content = new TextBlock { Text = $": {output.Type}" } + }; + OutputTabs.TabItems.Add(tab); + } + } + } + + private void RenderParameters(ScriptMetadata metadata) + { + ParametersPanel.Items.Clear(); + + foreach (var param in metadata.Parameters) + { + FrameworkElement control = null; + + switch (param.Type) + { + case ParameterType.Int: + control = new TextBox { Header = param.Description, Text = param.DefaultValue ?? string.Empty }; + break; + case ParameterType.String: + control = new TextBox { Header = param.Description, Text = param.DefaultValue ?? string.Empty }; + break; + case ParameterType.DateTime: + control = new DatePicker + { + Header = param.Description, + SelectedDate = DateTime.TryParse(param.DefaultValue, out var dt) ? dt : DateTime.Now + }; + break; + case ParameterType.Bool: + control = new CheckBox + { + Content = param.Description, + IsChecked = param.DefaultValue?.ToLower() == "true" + }; + break; + case ParameterType.Table: + var combo = new ComboBox { Header = param.Description }; + combo.Items.Add(" 1"); + combo.Items.Add(" 2"); + combo.Items.Add(" 3"); + control = combo; + break; + } + + param.Control = control; + ParametersPanel.Items.Add(control); + } + } + + private Dictionary CollectParameterValues() + { + var values = new Dictionary(); + + if (currentMetadata == null) return values; + + foreach (var param in currentMetadata.Parameters) + { + switch (param.Type) + { + case ParameterType.Int: + if (param.Control is TextBox tbInt && int.TryParse(tbInt.Text, out var intVal)) + values[param.Name] = intVal; + break; + case ParameterType.String: + if (param.Control is TextBox tbStr) + values[param.Name] = tbStr.Text; + break; + case ParameterType.DateTime: + if (param.Control is DatePicker dp && dp.SelectedDate.HasValue) + values[param.Name] = dp.SelectedDate.Value; + break; + case ParameterType.Bool: + if (param.Control is CheckBox cb) + values[param.Name] = cb.IsChecked ?? false; + break; + case ParameterType.Table: + if (param.Control is ComboBox combo && combo.SelectedItem != null) + values[param.Name] = combo.SelectedItem.ToString(); + break; + } + } + + return values; + } + + private void ExecuteButton_Click(object sender, RoutedEventArgs e) + { + var values = CollectParameterValues(); + + // SQL + foreach (var kv in values) + { + Console.WriteLine($"{kv.Key} = {kv.Value}"); + } + } + } +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Factories/ControlFactory.cs b/SQLVision.Visualizers/Factories/ControlFactory.cs new file mode 100644 index 0000000..0ac3298 --- /dev/null +++ b/SQLVision.Visualizers/Factories/ControlFactory.cs @@ -0,0 +1,599 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using SQLVision.Core.Enums; +using SQLVision.Core.Interfaces; +using SQLVision.Core.Models; +using SQLVision.Visualizers.Interfaces; +using System.Text.Json; +using Windows.Storage.Pickers; +using WinRT.Interop; + +namespace SQLVision.Visualizers.Factories; + +public class ControlFactory : IControlFactory +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly ISqlExecutionService? _executionService; + + public ControlFactory( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + + _executionService = serviceProvider.GetService(); + } + + public FrameworkElement CreateControl(ScriptParameter parameter, Action onValueChanged) + { + try + { + return parameter.Type switch + { + ParameterType.String => CreateTextBox(parameter, onValueChanged), + ParameterType.Integer => CreateNumberBox(parameter, onValueChanged), + ParameterType.Decimal => CreateNumberBox(parameter, onValueChanged, true), + ParameterType.DateTime => CreateDatePicker(parameter, onValueChanged), + ParameterType.Boolean => CreateCheckBox(parameter, onValueChanged), + ParameterType.Table => CreateComboBox(parameter, onValueChanged), + ParameterType.MultiSelect => CreateListView(parameter, onValueChanged), + ParameterType.Color => CreateColorPicker(parameter, onValueChanged), + ParameterType.File => CreateFilePicker(parameter, onValueChanged), + ParameterType.Json => CreateJsonEditor(parameter, onValueChanged), + _ => CreateTextBox(parameter, onValueChanged) + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating control for parameter {Parameter}", parameter.Name); + return CreateFallbackControl(parameter, onValueChanged); + } + } + + private FrameworkElement CreateTextBox(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var textBox = new TextBox + { + PlaceholderText = parameter.Watermark ?? $"Введите {parameter.EffectiveDisplayName.ToLower()}", + Text = parameter.DefaultValue?.ToString() ?? string.Empty + }; + + textBox.TextChanged += (s, e) => onValueChanged(textBox.Text); + + stackPanel.Children.Add(header); + stackPanel.Children.Add(textBox); + + return stackPanel; + } + + private FrameworkElement CreateNumberBox(ScriptParameter parameter, Action onValueChanged, bool isDecimal = false) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var numberBox = new NumberBox + { + PlaceholderText = parameter.Watermark ?? $"Введите {parameter.EffectiveDisplayName.ToLower()}", + SmallChange = isDecimal ? 0.1 : 1, + LargeChange = isDecimal ? 1 : 10, + SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Inline, + AcceptsExpression = false + }; + + if (isDecimal) + { + numberBox.Value = Convert.ToDouble(parameter.DefaultValue ?? 0); + numberBox.ValueChanged += (s, e) => onValueChanged(e.NewValue); + } + else + { + numberBox.Value = Convert.ToInt32(parameter.DefaultValue ?? 0); + numberBox.ValueChanged += (s, e) => onValueChanged((int)e.NewValue); + } + + if (parameter.ValidationRules != null) + { + if (parameter.ValidationRules.TryGetValue("min", out var min)) + numberBox.Minimum = Convert.ToDouble(min); + + if (parameter.ValidationRules.TryGetValue("max", out var max)) + numberBox.Maximum = Convert.ToDouble(max); + } + + stackPanel.Children.Add(header); + stackPanel.Children.Add(numberBox); + + return stackPanel; + } + + private FrameworkElement CreateDatePicker(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var datePicker = new DatePicker + { + Date = parameter.DefaultValue is DateTime defaultDate + ? new DateTimeOffset(defaultDate) + : DateTimeOffset.Now + }; + + datePicker.DateChanged += (s, e) => onValueChanged(e.NewDate.DateTime); + + if (parameter.ValidationRules != null) + { + if (parameter.ValidationRules.TryGetValue("mindate", out var minDate) && + minDate is string minDateStr && DateTime.TryParse(minDateStr, out var minDateTime)) + { + datePicker.MinYear = new DateTimeOffset(minDateTime); + } + + if (parameter.ValidationRules.TryGetValue("maxdate", out var maxDate) && + maxDate is string maxDateStr && DateTime.TryParse(maxDateStr, out var maxDateTime)) + { + datePicker.MaxYear = new DateTimeOffset(maxDateTime); + } + } + + stackPanel.Children.Add(header); + stackPanel.Children.Add(datePicker); + + return stackPanel; + } + + private FrameworkElement CreateCheckBox(ScriptParameter parameter, Action onValueChanged) + { + var checkBox = new CheckBox + { + Content = parameter.EffectiveDisplayName, + IsChecked = parameter.DefaultValue is bool defaultBool ? defaultBool : false + }; + + checkBox.Checked += (s, e) => onValueChanged(true); + checkBox.Unchecked += (s, e) => onValueChanged(false); + + return checkBox; + } + + private FrameworkElement CreateComboBox(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var comboBox = new ComboBox + { + PlaceholderText = parameter.Watermark ?? $"Выберите {parameter.EffectiveDisplayName.ToLower()}", + DisplayMemberPath = parameter.DisplayMember, + SelectedValuePath = parameter.ValueMember + }; + + LoadComboBoxDataAsync(comboBox, parameter).ConfigureAwait(false); + + comboBox.SelectionChanged += (s, e) => + { + if (comboBox.SelectedValue != null) + onValueChanged(comboBox.SelectedValue); + }; + + stackPanel.Children.Add(header); + stackPanel.Children.Add(comboBox); + + return stackPanel; + } + + private async Task LoadComboBoxDataAsync(ComboBox comboBox, ScriptParameter parameter) + { + if (string.IsNullOrEmpty(parameter.TableQuery) || _executionService == null) + return; + + try + { + var connectionString = "Server=localhost;Database=master;Trusted_Connection=True;"; + + var dataTable = await _executionService.LoadComboBoxDataAsync( + parameter.TableQuery, + connectionString, + DatabaseProvider.SqlServer); + + await comboBox.DispatcherQueue.EnqueueAsync(() => + { + comboBox.ItemsSource = dataTable.DefaultView; + + if (parameter.DefaultValue != null) + { + comboBox.SelectedValue = parameter.DefaultValue; + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading combo box data for {Parameter}", parameter.Name); + + await comboBox.DispatcherQueue.EnqueueAsync(() => + { + comboBox.ItemsSource = new List { "Ошибка загрузки данных" }; + comboBox.IsEnabled = false; + }); + } + } + + private FrameworkElement CreateListView(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var listView = new ListView + { + SelectionMode = ListViewSelectionMode.Multiple + }; + + // TODO: Загрузка данных для ListView + listView.SelectionChanged += (s, e) => + { + var selectedItems = new List(); + foreach (var item in listView.SelectedItems) + { + selectedItems.Add(item); + } + onValueChanged(selectedItems); + }; + + stackPanel.Children.Add(header); + stackPanel.Children.Add(listView); + + return stackPanel; + } + + private FrameworkElement CreateColorPicker(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var colorPicker = new ColorPicker + { + ColorSpectrumShape = ColorSpectrumShape.Box, + IsMoreButtonVisible = true, + IsColorSliderVisible = true, + IsColorChannelTextInputVisible = true, + IsHexInputVisible = true + }; + + // Установка цвета по умолчанию с нашей реализацией ParseColor + if (parameter.DefaultValue is string defaultColor) + { + try + { + colorPicker.Color = ParseColor(defaultColor); + } + catch + { + // Если не удалось распарсить, оставляем цвет по умолчанию + _logger.LogWarning("Failed to parse color: {Color}", defaultColor); + } + } + + colorPicker.ColorChanged += (s, e) => + onValueChanged($"#{e.NewColor.R:X2}{e.NewColor.G:X2}{e.NewColor.B:X2}"); + + stackPanel.Children.Add(header); + stackPanel.Children.Add(colorPicker); + + return stackPanel; + } + + // Наша собственная реализация парсинга цвета (без Microsoft.Toolkit.Uwp) + private Windows.UI.Color ParseColor(string colorString) + { + if (string.IsNullOrEmpty(colorString)) + return Windows.UI.Colors.Black; + + // Удаляем # + colorString = colorString.Trim().TrimStart('#'); + + try + { + if (colorString.Length == 6) + { + // RRGGBB + var r = Convert.ToByte(colorString.Substring(0, 2), 16); + var g = Convert.ToByte(colorString.Substring(2, 2), 16); + var b = Convert.ToByte(colorString.Substring(4, 2), 16); + return Windows.UI.Color.FromArgb(255, r, g, b); + } + else if (colorString.Length == 8) + { + // AARRGGBB + var a = Convert.ToByte(colorString.Substring(0, 2), 16); + var r = Convert.ToByte(colorString.Substring(2, 2), 16); + var g = Convert.ToByte(colorString.Substring(4, 2), 16); + var b = Convert.ToByte(colorString.Substring(6, 2), 16); + return Windows.UI.Color.FromArgb(a, r, g, b); + } + else if (colorString.Length == 3) + { + // RGB + var r = Convert.ToByte(new string(colorString[0], 2), 16); + var g = Convert.ToByte(new string(colorString[1], 2), 16); + var b = Convert.ToByte(new string(colorString[2], 2), 16); + return Windows.UI.Color.FromArgb(255, r, g, b); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to parse color string: {ColorString}", colorString); + } + + return Windows.UI.Colors.Black; + } + + private FrameworkElement CreateFilePicker(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var contentStack = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 8 + }; + + var textBox = new TextBox + { + PlaceholderText = "Путь к файлу", + Width = 200, + IsReadOnly = true + }; + + var button = new Button + { + Content = "Выбрать" + }; + + button.Click += async (s, e) => + { + // Получаем активное окно для инициализации FileOpenPicker + var window = Application.Current as App; + var hwnd = IntPtr.Zero; + + if (window?.MainWindow != null) + { + hwnd = WindowNative.GetWindowHandle(window.MainWindow); + } + + var openPicker = new FileOpenPicker(); + + if (hwnd != IntPtr.Zero) + { + InitializeWithWindow.Initialize(openPicker, hwnd); + } + + openPicker.ViewMode = PickerViewMode.List; + openPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + + if (parameter.ValidationRules != null && + parameter.ValidationRules.TryGetValue("extensions", out var extensions)) + { + foreach (var ext in extensions.ToString()!.Split(',')) + { + openPicker.FileTypeFilter.Add(ext.Trim()); + } + } + else + { + openPicker.FileTypeFilter.Add("*"); + } + + var file = await openPicker.PickSingleFileAsync(); + if (file != null) + { + textBox.Text = file.Path; + onValueChanged(file.Path); + } + }; + + contentStack.Children.Add(textBox); + contentStack.Children.Add(button); + + stackPanel.Children.Add(header); + stackPanel.Children.Add(contentStack); + + return stackPanel; + } + + private FrameworkElement CreateJsonEditor(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var textBox = new TextBox + { + PlaceholderText = "Введите JSON", + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + Height = 100, + FontFamily = new FontFamily("Consolas") + }; + + if (parameter.DefaultValue != null) + { + try + { + textBox.Text = JsonSerializer.Serialize( + parameter.DefaultValue, + new JsonSerializerOptions { WriteIndented = true }); + } + catch + { + textBox.Text = parameter.DefaultValue.ToString(); + } + } + + textBox.TextChanged += (s, e) => + { + try + { + var json = JsonSerializer.Deserialize(textBox.Text); + onValueChanged(json); + } + catch + { + // Игнорируем ошибки парсинга JSON + } + }; + + stackPanel.Children.Add(header); + stackPanel.Children.Add(textBox); + + return stackPanel; + } + + private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action onValueChanged) + { + var stackPanel = new StackPanel { Spacing = 4 }; + + var header = new TextBlock + { + Text = parameter.EffectiveDisplayName, + FontWeight = FontWeights.SemiBold + }; + + var textBox = new TextBox + { + Text = $"Ошибка создания контрола для типа {parameter.Type}", + IsReadOnly = true, + Foreground = new SolidColorBrush(Windows.UI.Colors.Red) + }; + + stackPanel.Children.Add(header); + stackPanel.Children.Add(textBox); + + return stackPanel; + } + + public void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary currentValues) + { + // Обновление состояния контрола на основе зависимостей + if (!string.IsNullOrEmpty(parameter.DependsOn)) + { + var isEnabled = CheckDependency(parameter, currentValues); + control.IsEnabled = isEnabled; + + if (!isEnabled) + { + ResetControlValue(control); + } + } + } + + private bool CheckDependency(ScriptParameter parameter, Dictionary currentValues) + { + if (!currentValues.TryGetValue(parameter.DependsOn, out var dependencyValue)) + return false; + + if (parameter.DependencyValues != null) + { + return parameter.DependencyValues + .Any(kvp => object.Equals(kvp.Value, dependencyValue)); + } + + return dependencyValue != null && !string.IsNullOrWhiteSpace(dependencyValue.ToString()); + } + + private void ResetControlValue(FrameworkElement control) + { + // Ищем первый дочерний элемент нужного типа + var child = FindChildOfType(control) ?? + FindChildOfType(control) ?? + FindChildOfType(control) ?? + FindChildOfType(control) ?? + FindChildOfType(control); + + switch (child) + { + case TextBox textBox: + textBox.Text = string.Empty; + break; + case ComboBox comboBox: + comboBox.SelectedIndex = -1; + break; + case CheckBox checkBox: + checkBox.IsChecked = false; + break; + case DatePicker datePicker: + datePicker.Date = DateTimeOffset.Now; + break; + case NumberBox numberBox: + numberBox.Value = 0; + break; + } + } + + private T? FindChildOfType(DependencyObject parent) where T : DependencyObject + { + var queue = new Queue(); + queue.Enqueue(parent); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (current is T result) + return result; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++) + { + queue.Enqueue(VisualTreeHelper.GetChild(current, i)); + } + } + + return null; + } +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Factories/VisualizerFactory.cs b/SQLVision.Visualizers/Factories/VisualizerFactory.cs new file mode 100644 index 0000000..31d1fe0 --- /dev/null +++ b/SQLVision.Visualizers/Factories/VisualizerFactory.cs @@ -0,0 +1,40 @@ +using SQLVision.Core.Enums; +using SQLVision.Visualizers.Interfaces; +using SQLVision.Visualizers.Visualizers; + +namespace SQLVision.Visualizers.Factories; + +public class VisualizerFactory : IVisualizerFactory +{ + private readonly Dictionary _visualizers; + private readonly IServiceProvider _serviceProvider; + + public VisualizerFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _visualizers = new Dictionary + { + [OutputType.Table] = new TableVisualizer(), + [OutputType.Chart] = new ChartVisualizer(), + [OutputType.Text] = new TextVisualizer(), + [OutputType.Grid] = new TableVisualizer(), // Пока используем TableVisualizer + [OutputType.Custom] = new TableVisualizer() // Fallback + }; + } + + public IVisualizer GetVisualizer(OutputType type) + { + if (_visualizers.TryGetValue(type, out var visualizer)) + { + return visualizer; + } + + // Fallback на табличный визуализатор + return _visualizers[OutputType.Table]; + } + + public void RegisterVisualizer(OutputType type, IVisualizer visualizer) + { + _visualizers[type] = visualizer; + } +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Interfaces/IControlFactory.cs b/SQLVision.Visualizers/Interfaces/IControlFactory.cs new file mode 100644 index 0000000..2ef5df3 --- /dev/null +++ b/SQLVision.Visualizers/Interfaces/IControlFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.UI.Xaml; +using SQLVision.Core.Models; + +namespace SQLVision.Visualizers.Interfaces; + +public interface IControlFactory +{ + FrameworkElement CreateControl(ScriptParameter parameter, Action onValueChanged); + void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary currentValues); +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Interfaces/IVisualizer.cs b/SQLVision.Visualizers/Interfaces/IVisualizer.cs new file mode 100644 index 0000000..d13b7c2 --- /dev/null +++ b/SQLVision.Visualizers/Interfaces/IVisualizer.cs @@ -0,0 +1,12 @@ +using Microsoft.UI.Xaml; +using SQLVision.Core.Enums; +using SQLVision.Core.Models; +using System.Data; + +namespace SQLVision.Visualizers.Interfaces; + +public interface IVisualizer +{ + FrameworkElement Visualize(DataTable data, OutputDefinition definition); + bool CanVisualize(OutputType type); +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs b/SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs new file mode 100644 index 0000000..46c1bce --- /dev/null +++ b/SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs @@ -0,0 +1,9 @@ +using SQLVision.Core.Enums; + +namespace SQLVision.Visualizers.Interfaces; + +public interface IVisualizerFactory +{ + IVisualizer GetVisualizer(OutputType type); + void RegisterVisualizer(OutputType type, IVisualizer visualizer); +} \ No newline at end of file diff --git a/SQLVision.Visualizers/SQLVision.Visualizers.csproj b/SQLVision.Visualizers/SQLVision.Visualizers.csproj new file mode 100644 index 0000000..412670e --- /dev/null +++ b/SQLVision.Visualizers/SQLVision.Visualizers.csproj @@ -0,0 +1,25 @@ + + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + enable + enable + true + 10.0.17763.0 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.Visualizers/ServiceExtensions.cs b/SQLVision.Visualizers/ServiceExtensions.cs new file mode 100644 index 0000000..9c38bdc --- /dev/null +++ b/SQLVision.Visualizers/ServiceExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using SQLVision.Visualizers.Factories; +using SQLVision.Visualizers.Interfaces; + +namespace SQLVision.Visualizers; + +public static class ServiceExtensions +{ + public static IServiceCollection AddSqlVisionVisualizers(this IServiceCollection services) + { + // Регистрация фабрик + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Visualizers/ChartVisualizer.cs b/SQLVision.Visualizers/Visualizers/ChartVisualizer.cs new file mode 100644 index 0000000..df64e73 --- /dev/null +++ b/SQLVision.Visualizers/Visualizers/ChartVisualizer.cs @@ -0,0 +1,184 @@ +using SQLVision.Core.Enums; +using SQLVision.Core.Models; +using SQLVision.Visualizers.Interfaces; +using System.Data; + +namespace SQLVision.Visualizers.Visualizers; + +public class ChartVisualizer : IVisualizer +{ + public FrameworkElement Visualize(DataTable data, OutputDefinition definition) + { + if (data.Rows.Count == 0) + { + return CreateEmptyChartMessage("Нет данных для построения графика"); + } + + try + { + var chartType = definition.ChartType; + var cartesianChart = new CartesianChart + { + Series = CreateSeries(data, definition), + XAxes = CreateXAxes(data, definition), + YAxes = CreateYAxes(definition), + LegendPosition = LegendPosition.Right, + TooltipPosition = LiveChartsCore.Measure.TooltipPosition.Hidden + }; + + return cartesianChart; + } + catch (Exception) + { + return CreateEmptyChartMessage("Ошибка при построении графика"); + } + } + + private ISeries[] CreateSeries(DataTable data, OutputDefinition definition) + { + var series = new List(); + + if (!string.IsNullOrEmpty(definition.SeriesColumn)) + { + // Разделение по сериям + var seriesGroups = data.AsEnumerable() + .GroupBy(row => row[definition.SeriesColumn]) + .ToList(); + + foreach (var group in seriesGroups) + { + var seriesName = group.Key.ToString(); + var values = group.Select(row => + { + if (string.IsNullOrEmpty(definition.YAxisColumn)) + return Convert.ToDouble(row[1]); + return Convert.ToDouble(row[definition.YAxisColumn]); + }).ToArray(); + + series.Add(CreateSeriesByType(definition.ChartType, values, seriesName)); + } + } + else + { + // Одна серия + var values = data.AsEnumerable() + .Select(row => + { + if (string.IsNullOrEmpty(definition.YAxisColumn)) + return Convert.ToDouble(row[1]); + return Convert.ToDouble(row[definition.YAxisColumn]); + }).ToArray(); + + series.Add(CreateSeriesByType(definition.ChartType, values, definition.Description)); + } + + return series.ToArray(); + } + + private ISeries CreateSeriesByType(ChartType chartType, double[] values, string name) + { + return chartType switch + { + ChartType.Line => new LineSeries + { + Values = values, + Name = name, + Fill = null, + GeometrySize = 8, + LineSmoothness = 0 + }, + ChartType.Bar => new ColumnSeries + { + Values = values, + Name = name + }, + ChartType.Area => new LineSeries + { + Values = values, + Name = name, + Fill = new SolidColorPaint(SKColors.Blue.WithAlpha(50)) + }, + ChartType.Scatter => new ScatterSeries + { + Values = values.Select((v, i) => new ObservablePoint(i, v)), + Name = name, + GeometrySize = 10 + }, + _ => new LineSeries + { + Values = values, + Name = name + } + }; + } + + private Axis[] CreateXAxes(DataTable data, OutputDefinition definition) + { + var labels = new List(); + + if (!string.IsNullOrEmpty(definition.XAxisColumn)) + { + labels = data.AsEnumerable() + .Select(row => row[definition.XAxisColumn].ToString()) + .ToList(); + } + else if (data.Columns.Count > 0) + { + // Берем первый столбец для оси X + labels = data.AsEnumerable() + .Select(row => row[0].ToString()) + .ToList(); + } + else + { + labels = Enumerable.Range(0, data.Rows.Count) + .Select(i => i.ToString()) + .ToList(); + } + + return new[] + { + new Axis + { + Labels = labels.ToArray(), + LabelsRotation = labels.Count > 10 ? 45 : 0, + TextSize = 12 + } + }; + } + + private Axis[] CreateYAxes(OutputDefinition definition) + { + return new[] + { + new Axis + { + Name = string.IsNullOrEmpty(definition.YAxisColumn) ? "Значения" : definition.YAxisColumn, + TextSize = 12 + } + }; + } + + private FrameworkElement CreateEmptyChartMessage(string message) + { + var textBlock = new TextBlock + { + Text = message, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FontSize = 16, + Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Gray) + }; + + var border = new Border + { + Child = textBlock, + Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent), + Padding = new Thickness(20) + }; + + return border; + } + + public bool CanVisualize(OutputType type) => type == OutputType.Chart; +} diff --git a/SQLVision.Visualizers/Visualizers/TableVisualizer.cs b/SQLVision.Visualizers/Visualizers/TableVisualizer.cs new file mode 100644 index 0000000..d31d07d --- /dev/null +++ b/SQLVision.Visualizers/Visualizers/TableVisualizer.cs @@ -0,0 +1,75 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using SQLVision.Core.Enums; +using SQLVision.Core.Models; +using SQLVision.Visualizers.Interfaces; +using System.Data; + +namespace SQLVision.Visualizers.Visualizers; + +public class TableVisualizer : IVisualizer +{ + public FrameworkElement Visualize(DataTable data, OutputDefinition definition) + { + var listView = new ListView + { + ItemsSource = data.DefaultView, + SelectionMode = ListViewSelectionMode.None, + IsItemClickEnabled = false, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + // Автоматическое создание колонок + listView.ItemTemplate = CreateDataTemplate(data); + + var scrollViewer = new ScrollViewer + { + Content = listView, + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + MaxHeight = 600 + }; + + return scrollViewer; + } + + private DataTemplate CreateDataTemplate(DataTable data) + { + var gridFactory = new FrameworkElementFactory(typeof(Grid)); + + // Создаем колонки + foreach (DataColumn column in data.Columns) + { + var columnDefinition = new ColumnDefinition(); + gridFactory.AppendChild(columnDefinition); + } + + // Создаем строку с текстовыми блоками + var stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel)); + stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal); + + foreach (DataColumn column in data.Columns) + { + var borderFactory = new FrameworkElementFactory(typeof(Border)); + borderFactory.SetValue(Border.BorderThicknessProperty, new Thickness(0, 0, 1, 1)); + borderFactory.SetValue(Border.BorderBrushProperty, new SolidColorBrush(Colors.LightGray)); + + var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock)); + textBlockFactory.SetBinding(TextBlock.TextProperty, + new Microsoft.UI.Xaml.Data.Binding { Path = new PropertyPath($"[{column.ColumnName}]") }); + textBlockFactory.SetValue(TextBlock.MarginProperty, new Thickness(4)); + textBlockFactory.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center); + + borderFactory.AppendChild(textBlockFactory); + stackPanelFactory.AppendChild(borderFactory); + } + + gridFactory.AppendChild(stackPanelFactory); + + return new DataTemplate { VisualTree = gridFactory }; + } + + public bool CanVisualize(OutputType type) => type == OutputType.Table; +} \ No newline at end of file diff --git a/SQLVision.Visualizers/Visualizers/TextVisualizer.cs b/SQLVision.Visualizers/Visualizers/TextVisualizer.cs new file mode 100644 index 0000000..2247210 --- /dev/null +++ b/SQLVision.Visualizers/Visualizers/TextVisualizer.cs @@ -0,0 +1,70 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using SQLVision.Core.Enums; +using SQLVision.Core.Models; +using SQLVision.Visualizers.Interfaces; +using System.Data; +using System.Text; + +namespace SQLVision.Visualizers.Visualizers; + +public class TextVisualizer : IVisualizer +{ + public FrameworkElement Visualize(DataTable data, OutputDefinition definition) + { + var textBlock = new TextBlock + { + Text = ConvertDataTableToText(data), + TextWrapping = TextWrapping.Wrap, + FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Consolas"), + FontSize = 12, + IsTextSelectionEnabled = true + }; + + return new ScrollViewer + { + Content = textBlock, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto + }; + } + + private string ConvertDataTableToText(DataTable data) + { + var sb = new StringBuilder(); + + // Заголовки + for (int i = 0; i < data.Columns.Count; i++) + { + sb.Append(data.Columns[i].ColumnName); + if (i < data.Columns.Count - 1) + sb.Append(" | "); + } + sb.AppendLine(); + sb.AppendLine(new string('-', data.Columns.Count * 20)); + + // Данные + foreach (DataRow row in data.Rows) + { + for (int i = 0; i < data.Columns.Count; i++) + { + var value = row[i]; + var text = value?.ToString() ?? "NULL"; + + // Обрезаем слишком длинные значения + if (text.Length > 50) + text = text.Substring(0, 47) + "..."; + + sb.Append(text); + + if (i < data.Columns.Count - 1) + sb.Append(" | "); + } + sb.AppendLine(); + } + + return sb.ToString(); + } + + public bool CanVisualize(OutputType type) => type == OutputType.Text; +} \ No newline at end of file diff --git a/SQLVision.Visualizers/app.manifest b/SQLVision.Visualizers/app.manifest new file mode 100644 index 0000000..89bcbfe --- /dev/null +++ b/SQLVision.Visualizers/app.manifest @@ -0,0 +1,26 @@ + + + + + + + PerMonitorV2 + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQLVision.slnx b/SQLVision.slnx new file mode 100644 index 0000000..2692cfc --- /dev/null +++ b/SQLVision.slnx @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/SQLVision/App.xaml b/SQLVision/App.xaml new file mode 100644 index 0000000..5dea64e --- /dev/null +++ b/SQLVision/App.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + Segoe Fluent Icons + + + + + + + + + + + + + diff --git a/SQLVision/App.xaml.cs b/SQLVision/App.xaml.cs new file mode 100644 index 0000000..17ccecb --- /dev/null +++ b/SQLVision/App.xaml.cs @@ -0,0 +1,35 @@ +using Microsoft.UI.Xaml; +using SQLVision.Views; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace SQLVision +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + private Window? _window; + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + _window = new MainWindow(); + _window.Activate(); + } + } +} diff --git a/SQLVision/Assets/LockScreenLogo.scale-200.png b/SQLVision/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/SQLVision/Assets/LockScreenLogo.scale-200.png differ diff --git a/SQLVision/Assets/SplashScreen.scale-200.png b/SQLVision/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..32f486a Binary files /dev/null and b/SQLVision/Assets/SplashScreen.scale-200.png differ diff --git a/SQLVision/Assets/Square150x150Logo.scale-200.png b/SQLVision/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..53ee377 Binary files /dev/null and b/SQLVision/Assets/Square150x150Logo.scale-200.png differ diff --git a/SQLVision/Assets/Square44x44Logo.scale-200.png b/SQLVision/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..f713bba Binary files /dev/null and b/SQLVision/Assets/Square44x44Logo.scale-200.png differ diff --git a/SQLVision/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/SQLVision/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..dc9f5be Binary files /dev/null and b/SQLVision/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/SQLVision/Assets/StoreLogo.png b/SQLVision/Assets/StoreLogo.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/SQLVision/Assets/StoreLogo.png differ diff --git a/SQLVision/Assets/Wide310x150Logo.scale-200.png b/SQLVision/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..8b4a5d0 Binary files /dev/null and b/SQLVision/Assets/Wide310x150Logo.scale-200.png differ diff --git a/SQLVision/Converters/BoolToVisibilityConverter.cs b/SQLVision/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..71e1b9f --- /dev/null +++ b/SQLVision/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,14 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using System; + +namespace SQLVision.Converters; + +public sealed class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + => value is bool b && b ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, string language) + => throw new NotImplementedException(); +} diff --git a/SQLVision/Converters/DataTableToEnumerableConverter.cs b/SQLVision/Converters/DataTableToEnumerableConverter.cs new file mode 100644 index 0000000..732c71e --- /dev/null +++ b/SQLVision/Converters/DataTableToEnumerableConverter.cs @@ -0,0 +1,34 @@ +using Microsoft.UI.Xaml.Data; +using System; +using System.Collections.Generic; +using System.Data; + +namespace SQLVision.Converters; + +public sealed class DataTableToEnumerableConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is DataTable dataTable) + { + // Преобразуем DataTable в список словарей или объектов + var result = new List>(); + + foreach (DataRow row in dataTable.Rows) + { + var dict = new Dictionary(); + foreach (DataColumn col in dataTable.Columns) + { + dict[col.ColumnName] = row[col]; + } + result.Add(dict); + } + return result; + } + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/SQLVision/Converters/ObjectToDateTimeOffsetConverter.cs b/SQLVision/Converters/ObjectToDateTimeOffsetConverter.cs new file mode 100644 index 0000000..ac03ac1 --- /dev/null +++ b/SQLVision/Converters/ObjectToDateTimeOffsetConverter.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml.Data; +using System; + +namespace SQLVision.Converters; + +public sealed class ObjectToDateTimeOffsetConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is DateTime dt) + return new DateTimeOffset(dt); + + return DateTimeOffset.Now; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is DateTimeOffset dto) + return dto.DateTime; + + return null; + } +} diff --git a/SQLVision/Converters/ObjectToNullableBoolConverter.cs b/SQLVision/Converters/ObjectToNullableBoolConverter.cs new file mode 100644 index 0000000..ddd638f --- /dev/null +++ b/SQLVision/Converters/ObjectToNullableBoolConverter.cs @@ -0,0 +1,34 @@ +using Microsoft.UI.Xaml.Data; +using System; + +namespace SQLVision.Converters; + +public sealed class ObjectToNullableBoolConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is bool b) + return b; + if (value is string s) + { + if (s.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else + { + return false; + } + } + + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is bool b) + return b; + + return false; + } +} diff --git a/SQLVision/Converters/OutputTypeToVisibilityConverter.cs b/SQLVision/Converters/OutputTypeToVisibilityConverter.cs new file mode 100644 index 0000000..c25fcaa --- /dev/null +++ b/SQLVision/Converters/OutputTypeToVisibilityConverter.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using SQLVision.Enums; +using System; + +namespace SQLVision.Converters; + +public sealed class OutputTypeToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not OutputType type || parameter is not string target) + return Visibility.Collapsed; + + return target switch + { + "Table" => type == OutputType.Table ? Visibility.Visible : Visibility.Collapsed, + _ => Visibility.Collapsed + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotImplementedException(); +} diff --git a/SQLVision/Converters/ParameterTypeToVisibilityConverter.cs b/SQLVision/Converters/ParameterTypeToVisibilityConverter.cs new file mode 100644 index 0000000..e997b8d --- /dev/null +++ b/SQLVision/Converters/ParameterTypeToVisibilityConverter.cs @@ -0,0 +1,27 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using SQLVision.Enums; +using System; + +namespace SQLVision.Converters; + +public sealed class ParameterTypeToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not ParameterType type || parameter is not string target) + return Visibility.Collapsed; + + return target switch + { + "Text" => (type == ParameterType.Int || type == ParameterType.String) ? Visibility.Visible : Visibility.Collapsed, + "Date" => type == ParameterType.DateTime ? Visibility.Visible : Visibility.Collapsed, + "Bool" => type == ParameterType.Bool ? Visibility.Visible : Visibility.Collapsed, + "Table" => type == ParameterType.Table ? Visibility.Visible : Visibility.Collapsed, + _ => Visibility.Collapsed + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotImplementedException(); +} diff --git a/SQLVision/Enums/OutputType.cs b/SQLVision/Enums/OutputType.cs new file mode 100644 index 0000000..ac638cb --- /dev/null +++ b/SQLVision/Enums/OutputType.cs @@ -0,0 +1,9 @@ +namespace SQLVision.Enums; + +public enum OutputType +{ + Table, + ChartLine, + ChartBar, + ChartPie +} diff --git a/SQLVision/Enums/ParameterType.cs b/SQLVision/Enums/ParameterType.cs new file mode 100644 index 0000000..277fbd2 --- /dev/null +++ b/SQLVision/Enums/ParameterType.cs @@ -0,0 +1,10 @@ +namespace SQLVision.Enums; + +public enum ParameterType +{ + Int, + String, + DateTime, + Bool, + Table +} diff --git a/SQLVision/Models/ConnectionProfile.cs b/SQLVision/Models/ConnectionProfile.cs new file mode 100644 index 0000000..16a5606 --- /dev/null +++ b/SQLVision/Models/ConnectionProfile.cs @@ -0,0 +1,12 @@ +namespace SQLVision.Models; + +public class ConnectionProfile +{ + public string Name { get; set; } = ""; + public string Server { get; set; } = ""; + public string Database { get; set; } = ""; + public string AuthType { get; set; } = "Windows"; + public string Login { get; set; } = ""; + public string Password { get; set; } = ""; + public string ConnectionString { get; set; } = ""; +} diff --git a/SQLVision/Models/ExecutionResult.cs b/SQLVision/Models/ExecutionResult.cs new file mode 100644 index 0000000..2a92aed --- /dev/null +++ b/SQLVision/Models/ExecutionResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Data; + +namespace SQLVision.Models; + +public sealed class ExecutionResult +{ + public string ScriptName { get; init; } = string.Empty; + public string FinalSql { get; init; } = string.Empty; + public IReadOnlyList Tables { get; init; } = Array.Empty(); +} diff --git a/SQLVision/Models/OutputDefinition.cs b/SQLVision/Models/OutputDefinition.cs new file mode 100644 index 0000000..015b78e --- /dev/null +++ b/SQLVision/Models/OutputDefinition.cs @@ -0,0 +1,9 @@ +using SQLVision.Enums; + +namespace SQLVision.Models; + +public sealed class OutputDefinition +{ + public OutputType Type { get; init; } + public string Title { get; init; } = string.Empty; +} diff --git a/SQLVision/Models/ParameterDefinition.cs b/SQLVision/Models/ParameterDefinition.cs new file mode 100644 index 0000000..6edfb27 --- /dev/null +++ b/SQLVision/Models/ParameterDefinition.cs @@ -0,0 +1,13 @@ +using SQLVision.Enums; + +namespace SQLVision.Models; + +public sealed class ParameterDefinition +{ + public string Name { get; init; } = string.Empty; + public ParameterType Type { get; init; } + public string DisplayName { get; init; } = string.Empty; + public string? DefaultValue { get; init; } + public string? TableQuery { get; init; } // для @table + public string? DependsOn { get; init; } // имя другого параметра +} diff --git a/SQLVision/Models/ParameterValueSet.cs b/SQLVision/Models/ParameterValueSet.cs new file mode 100644 index 0000000..f6f7828 --- /dev/null +++ b/SQLVision/Models/ParameterValueSet.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace SQLVision.Models; + +public sealed class ParameterValueSet +{ + public string ScriptFilePath { get; set; } = string.Empty; + public Dictionary Values { get; set; } = new(); +} diff --git a/SQLVision/Models/ScriptMetadata.cs b/SQLVision/Models/ScriptMetadata.cs new file mode 100644 index 0000000..c1292a8 --- /dev/null +++ b/SQLVision/Models/ScriptMetadata.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace SQLVision.Models; + +public sealed class ScriptMetadata +{ + public string FilePath { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string? Description { get; init; } + public IReadOnlyList Parameters { get; init; } = + Array.Empty(); + public IReadOnlyList Outputs { get; init; } = + Array.Empty(); +} diff --git a/SQLVision/Models/ScriptTreeNode.cs b/SQLVision/Models/ScriptTreeNode.cs new file mode 100644 index 0000000..0d7af09 --- /dev/null +++ b/SQLVision/Models/ScriptTreeNode.cs @@ -0,0 +1,13 @@ +using System.Collections.ObjectModel; + +namespace SQLVision.Models; + +public sealed class ScriptTreeNode +{ + public string Name { get; set; } = ""; + public string? FilePath { get; set; } + + public ObservableCollection Children { get; } = new(); + + public bool IsFolder => FilePath is null; +} diff --git a/SQLVision/Package.appxmanifest b/SQLVision/Package.appxmanifest new file mode 100644 index 0000000..3f753b7 --- /dev/null +++ b/SQLVision/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + SQLVision + frost + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SQLVision/Properties/launchSettings.json b/SQLVision/Properties/launchSettings.json new file mode 100644 index 0000000..7fcc934 --- /dev/null +++ b/SQLVision/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "SQLVision (Package)": { + "commandName": "MsixPackage" + }, + "SQLVision (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/SQLVision/SQLVision.csproj b/SQLVision/SQLVision.csproj new file mode 100644 index 0000000..ec026ea --- /dev/null +++ b/SQLVision/SQLVision.csproj @@ -0,0 +1,62 @@ + + + + WinExe + net8.0-windows10.0.19041.0 + 10.0.17763.0 + SQLVision + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + true + enable + true + None + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + diff --git a/SQLVision/Scripts/ServerInfo.sql b/SQLVision/Scripts/ServerInfo.sql new file mode 100644 index 0000000..f6888f3 --- /dev/null +++ b/SQLVision/Scripts/ServerInfo.sql @@ -0,0 +1,63 @@ +-- @description "Информация о сервере (с параметрами)" +-- @param IncludeMemory bool "Показывать информацию о памяти" default="true" +-- @param IncludeVersion bool "Показывать информацию о версии SQL Server" default="true" +-- @param ServerTag string "Метка сервера (произвольный текст)" default="Local" +-- @output table "Основная информация о сервере" +-- @output table "Память и конфигурация" +-- @output table "Версия SQL Server" + +/* +declare @ServerTag as nvarchar(150) +declare @IncludeMemory as bit +declare @IncludeVersion as bit +*/ + +------------------------------- +-- Основная информация о сервере +------------------------------- +SELECT + SERVERPROPERTY('MachineName') AS MachineName, + SERVERPROPERTY('ServerName') AS ServerName, + SERVERPROPERTY('InstanceName') AS InstanceName, + SERVERPROPERTY('Edition') AS Edition, + SERVERPROPERTY('ProductLevel') AS ProductLevel, + SERVERPROPERTY('ProductVersion') AS ProductVersion, + SERVERPROPERTY('IsClustered') AS IsClustered, + SERVERPROPERTY('IsHadrEnabled') AS IsHadrEnabled, + SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS PhysicalName, + @ServerTag AS ServerTag; + +------------------------------- +-- Память и конфигурация +------------------------------- +IF (@IncludeMemory = 1) +BEGIN + SELECT + total_physical_memory_kb / 1024 AS TotalMemoryMB, + available_physical_memory_kb / 1024 AS AvailableMemoryMB, + system_memory_state_desc AS MemoryState, + @ServerTag AS ServerTag + FROM sys.dm_os_sys_memory; +END +ELSE +BEGIN + SELECT + 'Memory block disabled by parameter' AS Message, + @ServerTag AS ServerTag; +END + +------------------------------- +-- Версия SQL Server +------------------------------- +IF (@IncludeVersion = 1) +BEGIN + SELECT + @@VERSION AS VersionString, + @ServerTag AS ServerTag; +END +ELSE +BEGIN + SELECT + 'Version block disabled by parameter' AS Message, + @ServerTag AS ServerTag; +END diff --git a/SQLVision/Selectors/OutputTemplateSelector.cs b/SQLVision/Selectors/OutputTemplateSelector.cs new file mode 100644 index 0000000..daf55cd --- /dev/null +++ b/SQLVision/Selectors/OutputTemplateSelector.cs @@ -0,0 +1,14 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace SQLVision.Selectors; + +public sealed class OutputTemplateSelector : DataTemplateSelector +{ + public DataTemplate? TableTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item) + { + return TableTemplate!; + } +} diff --git a/SQLVision/Services/ConnectionStorageService.cs b/SQLVision/Services/ConnectionStorageService.cs new file mode 100644 index 0000000..f06e68f --- /dev/null +++ b/SQLVision/Services/ConnectionStorageService.cs @@ -0,0 +1,42 @@ +using SQLVision.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace SQLVision.Services; + +public class ConnectionStorageService +{ + private readonly string _filePath; + + public ConnectionStorageService() + { + var folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appFolder = Path.Combine(folder, "SQLVision"); + + if (!Directory.Exists(appFolder)) + Directory.CreateDirectory(appFolder); + + _filePath = Path.Combine(appFolder, "connections.json"); + } + + public List Load() + { + if (!File.Exists(_filePath)) + return new List(); + + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize>(json) ?? new(); + } + + public void Save(List profiles) + { + var json = JsonSerializer.Serialize(profiles, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(_filePath, json); + } +} diff --git a/SQLVision/Services/ExcelExportService.cs b/SQLVision/Services/ExcelExportService.cs new file mode 100644 index 0000000..66d03d6 --- /dev/null +++ b/SQLVision/Services/ExcelExportService.cs @@ -0,0 +1,27 @@ +using ClosedXML.Excel; +using System.Collections.Generic; +using System.Data; + +namespace SQLVision.Services; + +public sealed class ExcelExportService +{ + public void ExportTablesToExcel(IReadOnlyList tables, string filePath) + { + using var workbook = new XLWorkbook(); + + for (int i = 0; i < tables.Count; i++) + { + var table = tables[i]; + var sheetName = string.IsNullOrWhiteSpace(table.TableName) + ? $"Sheet{i + 1}" + : table.TableName; + + var ws = workbook.Worksheets.Add(sheetName); + ws.Cell(1, 1).InsertTable(table); + ws.Columns().AdjustToContents(); + } + + workbook.SaveAs(filePath); + } +} diff --git a/SQLVision/Services/ParameterResolver.cs b/SQLVision/Services/ParameterResolver.cs new file mode 100644 index 0000000..61de807 --- /dev/null +++ b/SQLVision/Services/ParameterResolver.cs @@ -0,0 +1,42 @@ +using SQLVision.Enums; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace SQLVision.Services; + +public sealed class ParameterResolver +{ + public string RenderSqlForDisplay( + string originalSql, + IReadOnlyDictionary parameterValues, + IReadOnlyList definitions) + { + var sql = originalSql; + + foreach (var def in definitions) + { + parameterValues.TryGetValue(def.Name, out var value); + + var formatted = FormatValueForSql(def.Type, value); + sql = sql.Replace("@" + def.Name, formatted); + } + + return sql; + } + + private static string FormatValueForSql(ParameterType type, object? value) + { + if (value is null || value is DBNull) + return "NULL"; + + return type switch + { + ParameterType.Int => Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture), + ParameterType.Bool => ((bool)value) ? "1" : "0", + ParameterType.DateTime => $"'{((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}'", + ParameterType.String or ParameterType.Table => $"'{value.ToString()?.Replace("'", "''")}'", + _ => $"'{value.ToString()?.Replace("'", "''")}'" + }; + } +} diff --git a/SQLVision/Services/SqlCommentParser.cs b/SQLVision/Services/SqlCommentParser.cs new file mode 100644 index 0000000..d41bc77 --- /dev/null +++ b/SQLVision/Services/SqlCommentParser.cs @@ -0,0 +1,106 @@ +using SQLVision.Enums; +using SQLVision.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; + +namespace SQLVision.Services; + +public sealed class SqlCommentParser +{ + private static readonly Regex DescriptionRegex = + new(@"--\s*@description\s+""(.+)""", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex ParamRegex = + new(@"--\s*@param\s+(\w+)\s+(\w+)\s+""(.+?)""(?:\s+default=""(.*?)"")?(?:\s+@table\s+""(.+?)"")?(?:\s+dependsOn=""(.*?)"")?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex OutputRegex = + new(@"--\s*@output\s+([\w:]+)\s+""(.+?)""", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public ScriptMetadata Parse(string filePath) + { + var text = File.ReadAllText(filePath); + var lines = text.Split(Environment.NewLine); + + string? description = null; + var parameters = new List(); + var outputs = new List(); + + foreach (var line in lines) + { + var descMatch = DescriptionRegex.Match(line); + if (descMatch.Success) + { + description = descMatch.Groups[1].Value; + continue; + } + + var paramMatch = ParamRegex.Match(line); + if (paramMatch.Success) + { + var name = paramMatch.Groups[1].Value; + var typeStr = paramMatch.Groups[2].Value; + var displayName = paramMatch.Groups[3].Value; + var defaultValue = paramMatch.Groups[4].Success ? paramMatch.Groups[4].Value : null; + var tableQuery = paramMatch.Groups[5].Success ? paramMatch.Groups[5].Value : null; + var dependsOn = paramMatch.Groups[6].Success ? paramMatch.Groups[6].Value : null; + + var parameterType = typeStr.ToLower() switch + { + "int" => ParameterType.Int, + "string" => ParameterType.String, + "datetime" => ParameterType.DateTime, + "bool" => ParameterType.Bool, + "table" => ParameterType.Table, + _ => ParameterType.String + }; + + parameters.Add(new ParameterDefinition + { + Name = name, + Type = parameterType, + DisplayName = displayName, + DefaultValue = defaultValue, + TableQuery = tableQuery, + DependsOn = dependsOn + }); + + continue; + } + + var outputMatch = OutputRegex.Match(line); + if (outputMatch.Success) + { + var typeStr = outputMatch.Groups[1].Value; + var title = outputMatch.Groups[2].Value; + + var outputType = typeStr.ToLower() switch + { + "table" => OutputType.Table, + "chart:line" => OutputType.ChartLine, + "chart:bar" => OutputType.ChartBar, + "chart:pie" => OutputType.ChartPie, + _ => OutputType.Table + }; + + outputs.Add(new OutputDefinition + { + Type = outputType, + Title = title + }); + } + } + + return new ScriptMetadata + { + FilePath = filePath, + Name = Path.GetFileNameWithoutExtension(filePath), + Description = description, + Parameters = parameters, + Outputs = outputs + }; + } +} diff --git a/SQLVision/Services/SqlExecutor.cs b/SQLVision/Services/SqlExecutor.cs new file mode 100644 index 0000000..8f5d9c7 --- /dev/null +++ b/SQLVision/Services/SqlExecutor.cs @@ -0,0 +1,63 @@ +using Microsoft.Data.SqlClient; +using SQLVision.Models; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace SQLVision.Services; + +public sealed class SqlExecutor +{ + private string _connectionString; + + public SqlExecutor(string connectionString) + { + _connectionString = connectionString; + } + + public void SetConnect(string connectionString) + { + _connectionString = connectionString; + } + + public async Task ExecuteAsync( + ScriptMetadata metadata, + string sqlText, + IReadOnlyDictionary parameterValues, + CancellationToken cancellationToken = default) + { + // Здесь sqlText уже с параметрами вида @ParamName + var dataTables = new List(); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(cancellationToken); + + await using var command = new SqlCommand(sqlText, connection) + { + CommandType = CommandType.Text + }; + + foreach (var kv in parameterValues) + { + var paramName = "@" + kv.Key.Name; + command.Parameters.AddWithValue(paramName, kv.Value ?? DBNull.Value); + } + + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + do + { + var table = new DataTable(); + table.Load(reader); + dataTables.Add(table); + } while (!reader.IsClosed); + + return new ExecutionResult + { + ScriptName = metadata.Name, + FinalSql = sqlText, + Tables = dataTables + }; + } +} diff --git a/SQLVision/Services/TableDataProvider.cs b/SQLVision/Services/TableDataProvider.cs new file mode 100644 index 0000000..58b5a7b --- /dev/null +++ b/SQLVision/Services/TableDataProvider.cs @@ -0,0 +1,30 @@ +using Microsoft.Data.SqlClient; +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace SQLVision.Services; + +public sealed class TableDataProvider +{ + private readonly string _connectionString; + + public TableDataProvider(string connectionString) + { + _connectionString = connectionString; + } + + public async Task LoadTableAsync(string query, CancellationToken cancellationToken = default) + { + var table = new DataTable(); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(cancellationToken); + + await using var command = new SqlCommand(query, connection); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + table.Load(reader); + + return table; + } +} diff --git a/SQLVision/ViewModels/BaseViewModel.cs b/SQLVision/ViewModels/BaseViewModel.cs new file mode 100644 index 0000000..e03c0f9 --- /dev/null +++ b/SQLVision/ViewModels/BaseViewModel.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SQLVision.ViewModels; + +public abstract class BaseViewModel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected void RaisePropertyChanged([CallerMemberName] string? name = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? name = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return false; + + field = value; + RaisePropertyChanged(name); + return true; + } +} diff --git a/SQLVision/ViewModels/ConnectionViewModel.cs b/SQLVision/ViewModels/ConnectionViewModel.cs new file mode 100644 index 0000000..87c1b47 --- /dev/null +++ b/SQLVision/ViewModels/ConnectionViewModel.cs @@ -0,0 +1,109 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Data.SqlClient; +using SQLVision.Models; +using SQLVision.Services; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace SQLVision.ViewModels; + +public partial class ConnectionViewModel : ObservableObject +{ + [ObservableProperty] private string serverName = "."; + [ObservableProperty] private string authType = "Windows"; + [ObservableProperty] private string login = ""; + [ObservableProperty] private string password = ""; + [ObservableProperty] private ObservableCollection databases = new(); + [ObservableProperty] private string selectedDatabase = ""; + [ObservableProperty] private string statusMessage = ""; + + public bool IsSqlAuth => AuthType == "SQL Server"; + + public event Action? ConnectionSaved; + private readonly ConnectionStorageService _storage; + private List _profiles; + + public ConnectionViewModel() + { + _storage = new ConnectionStorageService(); + _profiles = _storage.Load(); + } + + [RelayCommand] + private async Task TestConnection() + { + try + { + var connStr = BuildConnectionString("master"); + + using var conn = new SqlConnection(connStr); + await conn.OpenAsync(); + + StatusMessage = "Подключение успешно. Загружаю базы..."; + + // Загружаем список баз + var cmd = new SqlCommand("SELECT name FROM sys.databases ORDER BY name", conn); + using var reader = await cmd.ExecuteReaderAsync(); + + Databases.Clear(); + while (await reader.ReadAsync()) + Databases.Add(reader.GetString(0)); + + StatusMessage = "Базы данных загружены."; + } + catch (Exception ex) + { + StatusMessage = "Ошибка: " + ex.Message; + } + } + + [RelayCommand] + private void Save() + { + var profile = new ConnectionProfile + { + Name = $"{ServerName} ({SelectedDatabase})", + Server = ServerName, + Database = SelectedDatabase, + AuthType = AuthType, + Login = Login, + Password = Password, + ConnectionString = BuildConnectionString(SelectedDatabase) + }; + + _profiles.RemoveAll(p => p.Name == profile.Name); + _profiles.Add(profile); + + _storage.Save(_profiles); + + StatusMessage = "Подключение сохранено."; + ConnectionSaved?.Invoke(profile); + } + + + private string BuildConnectionString(string database) + { + var builder = new SqlConnectionStringBuilder + { + DataSource = ServerName, + InitialCatalog = database, + TrustServerCertificate = true + }; + + if (IsSqlAuth) + { + builder.UserID = Login; + builder.Password = Password; + builder.IntegratedSecurity = false; + } + else + { + builder.IntegratedSecurity = true; + } + + return builder.ConnectionString; + } +} diff --git a/SQLVision/ViewModels/MainViewModel.cs b/SQLVision/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..7ab797d --- /dev/null +++ b/SQLVision/ViewModels/MainViewModel.cs @@ -0,0 +1,339 @@ +using Microsoft.UI.Xaml; +using SQLVision.Models; +using SQLVision.Services; +using SQLVision.Views; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace SQLVision.ViewModels; + +public sealed class MainViewModel : BaseViewModel +{ + private readonly SqlCommentParser _parser; + private readonly SqlExecutor _executor; + private readonly ParameterResolver _parameterResolver; + private readonly TableDataProvider _tableDataProvider; + private readonly ExcelExportService _excelExportService; + + // ----------------------------- + // Подключения + // ----------------------------- + private readonly ConnectionStorageService _storage; + + public ObservableCollection SavedConnections { get; } = new(); + + private ConnectionProfile? _selectedConnection; + public ConnectionProfile? SelectedConnection + { + get => _selectedConnection; + set + { + if (SetProperty(ref _selectedConnection, value)) + { + if (value != null) + ConnectionString = value.ConnectionString; + } + } + } + + private string _connectionString = ""; + public string ConnectionString + { + get => _connectionString; + set => SetProperty(ref _connectionString, value); + } + + // ----------------------------- + // Скрипты, дерево и параметры + // ----------------------------- + public ObservableCollection Scripts { get; } = new(); + + // Дерево скриптов для TreeView + public ObservableCollection ScriptTree { get; } = new(); + + private ScriptTreeNode? _selectedScriptNode; + public ScriptTreeNode? SelectedScriptNode + { + get => _selectedScriptNode; + set + { + if (SetProperty(ref _selectedScriptNode, value)) + { + if (value?.FilePath is not null) + { + // Находим ScriptMetadata по пути файла + var meta = Scripts.FirstOrDefault(s => + string.Equals(s.FilePath, value.FilePath, StringComparison.OrdinalIgnoreCase)); + + if (meta is not null) + SelectedScript = meta; + } + } + } + } + + public ObservableCollection Parameters { get; } = new(); + public ObservableCollection Outputs { get; } = new(); + + private ScriptMetadata? _selectedScript; + public ScriptMetadata? SelectedScript + { + get => _selectedScript; + set + { + if (SetProperty(ref _selectedScript, value)) + { + (ExecuteCommand as RelayCommand)?.RaiseCanExecuteChanged(); + _ = LoadParametersForSelectedScriptAsync(); + } + } + } + + private ExecutionResult? _lastResult; + public ExecutionResult? LastResult + { + get => _lastResult; + set + { + if (SetProperty(ref _lastResult, value)) + { + (ExportToExcelCommand as RelayCommand)?.RaiseCanExecuteChanged(); + _ = LoadParametersForSelectedScriptAsync(); + } + } + } + + // ----------------------------- + // Команды + // ----------------------------- + public ICommand ExecuteCommand { get; } + public ICommand ExportToExcelCommand { get; } + public ICommand ShowHelpCommand { get; } + public ICommand OpenConnectionCommand { get; } + + // ----------------------------- + // Конструктор + // ----------------------------- + public MainViewModel( + SqlCommentParser parser, + SqlExecutor executor, + ParameterResolver parameterResolver, + TableDataProvider tableDataProvider, + ExcelExportService excelExportService) + { + _parser = parser; + _executor = executor; + _parameterResolver = parameterResolver; + _tableDataProvider = tableDataProvider; + _excelExportService = excelExportService; + + ExecuteCommand = new RelayCommand(async _ => await ExecuteAsync(), _ => SelectedScript is not null); + ExportToExcelCommand = new RelayCommand(_ => ExportToExcel(), _ => LastResult is not null); + ShowHelpCommand = new RelayCommand(_ => ShowHelp()); + OpenConnectionCommand = new RelayCommand(_ => OpenConnection()); + + _storage = new ConnectionStorageService(); + + foreach (var p in _storage.Load()) + SavedConnections.Add(p); + + SelectedConnection = SavedConnections.FirstOrDefault(); + } + + // ----------------------------- + // Окно подключения + // ----------------------------- + private void OpenConnection() + { + var vm = new ConnectionViewModel(); + var page = new ConnectionPage { DataContext = vm }; + + vm.ConnectionSaved += profile => + { + var existing = SavedConnections.FirstOrDefault(p => p.Name == profile.Name); + if (existing is not null) + SavedConnections.Remove(existing); + + SavedConnections.Add(profile); + _storage.Save(SavedConnections.ToList()); + SelectedConnection = profile; + }; + + var window = new Window + { + Content = page + }; + window.Activate(); + } + + // ----------------------------- + // Help + // ----------------------------- + private void ShowHelp() + { + var window = new HelpWindow(); + window.Activate(); + } + + // ----------------------------- + // Загрузка скриптов и построение дерева + // ----------------------------- + public void LoadScripts(string scriptsFolder) + { + Scripts.Clear(); + ScriptTree.Clear(); + + if (!Directory.Exists(scriptsFolder)) + return; + + // Строим дерево по корневой папке + var rootNode = BuildScriptTree(scriptsFolder); + ScriptTree.Add(rootNode); + } + + private ScriptTreeNode BuildScriptTree(string folderPath) + { + var folderNode = new ScriptTreeNode + { + Name = Path.GetFileName(folderPath), + FilePath = null + }; + + // Подпапки + foreach (var dir in Directory.GetDirectories(folderPath)) + { + var subNode = BuildScriptTree(dir); + folderNode.Children.Add(subNode); + } + + // Файлы .sql + foreach (var file in Directory.GetFiles(folderPath, "*.sql")) + { + var meta = _parser.Parse(file); + Scripts.Add(meta); + + var fileNode = new ScriptTreeNode + { + Name = Path.GetFileName(file), + FilePath = file + }; + + folderNode.Children.Add(fileNode); + } + + return folderNode; + } + + // ----------------------------- + // Параметры + // ----------------------------- + private async Task LoadParametersForSelectedScriptAsync() + { + Parameters.Clear(); + Outputs.Clear(); + + if (SelectedScript is null) + return; + + foreach (var p in SelectedScript.Parameters) + { + var vm = new ParameterViewModel(p); + Parameters.Add(vm); + + if (p.TableQuery is not null) + { + var table = await _tableDataProvider.LoadTableAsync(p.TableQuery); + vm.LookupTable = table; + } + } + + foreach (var o in SelectedScript.Outputs) + Outputs.Add(new OutputViewModel(o)); + } + + // ----------------------------- + // Выполнение SQL + // ----------------------------- + private async Task ExecuteAsync() + { + if (SelectedScript is null) + return; + + if (string.IsNullOrWhiteSpace(ConnectionString)) + { + // TODO: вывести ContentDialog "Подключение не выбрано" + return; + } + + var sqlText = await File.ReadAllTextAsync(SelectedScript.FilePath); + + var paramValues = Parameters.ToDictionary( + p => p.Definition, + p => p.Value); + + // Если SqlExecutor умеет менять строку подключения динамически — здесь можно прокинуть ConnectionString + _executor.SetConnect(ConnectionString); + var result = await _executor.ExecuteAsync(SelectedScript, sqlText, paramValues); + + for (int i = 0; i < Outputs.Count; i++) + { + Outputs[i].Table = result.Tables[i]; + result.Tables[i].TableName = Outputs[i].Definition.Title; + } + + LastResult = result; + + + RaisePropertyChanged(nameof(Outputs)); + + SaveParameterValues(SelectedScript.FilePath, paramValues); + } + + // ----------------------------- + // Экспорт + // ----------------------------- + private void ExportToExcel() + { + if (LastResult is null || LastResult.Tables.Count == 0) + return; + + var directory = Path.Combine(AppContext.BaseDirectory, "Export"); + Directory.CreateDirectory(directory); + + var fileName = $"{LastResult.ScriptName}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"; + var fullPath = Path.Combine(directory, fileName); + + _excelExportService.ExportTablesToExcel(LastResult.Tables, fullPath); + } + + // ----------------------------- + // Сохранение параметров + // ----------------------------- + private void SaveParameterValues(string scriptPath, Dictionary values) + { + var directory = Path.Combine(AppContext.BaseDirectory, "Config", "Parameters"); + Directory.CreateDirectory(directory); + + var fileName = Path.GetFileNameWithoutExtension(scriptPath) + ".json"; + var fullPath = Path.Combine(directory, fileName); + + var payload = new ParameterValueSet + { + ScriptFilePath = scriptPath, + Values = values.ToDictionary(t => t.Key.Name, t => t.Value), + }; + + var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(fullPath, json); + } +} diff --git a/SQLVision/ViewModels/OutputViewModel.cs b/SQLVision/ViewModels/OutputViewModel.cs new file mode 100644 index 0000000..c489684 --- /dev/null +++ b/SQLVision/ViewModels/OutputViewModel.cs @@ -0,0 +1,16 @@ +using SQLVision.Models; +using System.Data; + +namespace SQLVision.ViewModels; + +public sealed class OutputViewModel : BaseViewModel +{ + public OutputDefinition Definition { get; } + public DataTable? Table { get; set; } // для OutputType.Table + // для графиков можно добавить коллекции серий и т.д. + + public OutputViewModel(OutputDefinition definition) + { + Definition = definition; + } +} diff --git a/SQLVision/ViewModels/ParameterViewModel.cs b/SQLVision/ViewModels/ParameterViewModel.cs new file mode 100644 index 0000000..ae6a11b --- /dev/null +++ b/SQLVision/ViewModels/ParameterViewModel.cs @@ -0,0 +1,46 @@ +using SQLVision.Enums; +using SQLVision.Models; +using System.Data; + +namespace SQLVision.ViewModels; + +public sealed class ParameterViewModel : BaseViewModel +{ + public ParameterDefinition Definition { get; } + + private object? _value; + public object? Value + { + get => _value; + set + { + if (!Equals(_value, value)) + { + _value = value; + RaisePropertyChanged(); + } + } + } + + private DataTable? _lookupTable; + public DataTable? LookupTable + { + get => _lookupTable; + set + { + if (!Equals(_lookupTable, value)) + { + _lookupTable = value; + RaisePropertyChanged(); + } + } + } + + public bool IsLookup => Definition.Type == ParameterType.Table; + + public ParameterViewModel(ParameterDefinition definition) + { + Definition = definition; + Value = definition.DefaultValue; + } +} diff --git a/SQLVision/ViewModels/RelayCommand.cs b/SQLVision/ViewModels/RelayCommand.cs new file mode 100644 index 0000000..bd15c74 --- /dev/null +++ b/SQLVision/ViewModels/RelayCommand.cs @@ -0,0 +1,24 @@ +using System; +using System.Windows.Input; + +namespace SQLVision.ViewModels; + +public sealed class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Predicate? _canExecute; + + public RelayCommand(Action execute, Predicate? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true; + + public void Execute(object? parameter) => _execute(parameter); + + public event EventHandler? CanExecuteChanged; + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/SQLVision/Views/ConnectionPage.xaml b/SQLVision/Views/ConnectionPage.xaml new file mode 100644 index 0000000..e022818 --- /dev/null +++ b/SQLVision/Views/ConnectionPage.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +