Добавьте файлы проекта.
12
SQLVision.Core/Enums/ChartType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum ChartType
|
||||
{
|
||||
Line,
|
||||
Bar,
|
||||
Pie,
|
||||
Area,
|
||||
Scatter,
|
||||
Heatmap,
|
||||
Candlestick
|
||||
}
|
||||
10
SQLVision.Core/Enums/DatabaseProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum DatabaseProvider
|
||||
{
|
||||
SqlServer, // Только MSSQL для начала
|
||||
// PostgreSQL,
|
||||
// MySQL,
|
||||
// SQLite,
|
||||
// Oracle
|
||||
}
|
||||
9
SQLVision.Core/Enums/NotificationType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Information,
|
||||
Success,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
11
SQLVision.Core/Enums/OutputType.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum OutputType
|
||||
{
|
||||
Table,
|
||||
Chart,
|
||||
Text,
|
||||
Grid,
|
||||
Map,
|
||||
Custom
|
||||
}
|
||||
15
SQLVision.Core/Enums/ParameterType.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum ParameterType
|
||||
{
|
||||
String,
|
||||
Integer,
|
||||
Decimal,
|
||||
DateTime,
|
||||
Boolean,
|
||||
Table, // Для ComboBox с данными из БД
|
||||
MultiSelect, // ListBox с множественным выбором
|
||||
Color,
|
||||
File,
|
||||
Json
|
||||
}
|
||||
9
SQLVision.Core/Enums/ScriptChangeType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SQLVision.Core.Enums;
|
||||
|
||||
public enum ScriptChangeType
|
||||
{
|
||||
Created,
|
||||
Updated,
|
||||
Deleted,
|
||||
Renamed
|
||||
}
|
||||
11
SQLVision.Core/Interfaces/IExportHandler.cs
Normal file
@@ -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<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
|
||||
}
|
||||
11
SQLVision.Core/Interfaces/IExportService.cs
Normal file
@@ -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<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
|
||||
}
|
||||
10
SQLVision.Core/Interfaces/IMemoryExportHandler.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SQLVision.Core.Interfaces;
|
||||
|
||||
public interface IMemoryExportHandler
|
||||
{
|
||||
Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
|
||||
}
|
||||
13
SQLVision.Core/Interfaces/IPluginContext.cs
Normal file
@@ -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);
|
||||
}
|
||||
13
SQLVision.Core/Interfaces/IPluginManager.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using SQLVision.Core.Models;
|
||||
|
||||
namespace SQLVision.Core.Interfaces;
|
||||
|
||||
public interface IPluginManager
|
||||
{
|
||||
void LoadPlugins(string pluginsDirectory);
|
||||
IEnumerable<ISqlVisionPlugin> GetPlugins();
|
||||
T? GetPlugin<T>() where T : ISqlVisionPlugin;
|
||||
|
||||
Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> parameters);
|
||||
Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result);
|
||||
}
|
||||
38
SQLVision.Core/Interfaces/IScriptManager.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
|
||||
namespace SQLVision.Core.Interfaces;
|
||||
|
||||
public interface IScriptManager
|
||||
{
|
||||
Task<IEnumerable<ScriptMetadata>> LoadScriptsAsync(string? directory = null);
|
||||
Task<ScriptMetadata> ReloadScriptAsync(string filePath);
|
||||
void WatchDirectory(string directory, Action<string> onScriptChanged);
|
||||
|
||||
event EventHandler<ScriptChangedEventArgs> ScriptChanged;
|
||||
event EventHandler<ScriptsReloadedEventArgs> 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<ScriptMetadata> Scripts { get; }
|
||||
|
||||
public ScriptsReloadedEventArgs(IEnumerable<ScriptMetadata> scripts)
|
||||
{
|
||||
Scripts = scripts;
|
||||
}
|
||||
}
|
||||
30
SQLVision.Core/Interfaces/ISqlExecutionService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Core.Interfaces;
|
||||
|
||||
public interface ISqlExecutionService
|
||||
{
|
||||
Task<ExecutionResult> ExecuteAsync(
|
||||
ScriptMetadata script,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ExecutionResult> ExecuteAsync(
|
||||
string sql,
|
||||
Dictionary<string, object> parameters,
|
||||
string connectionString,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> TestConnectionAsync(
|
||||
string connectionString,
|
||||
DatabaseProvider provider,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<DataTable> LoadComboBoxDataAsync(
|
||||
string query,
|
||||
string connectionString,
|
||||
DatabaseProvider provider,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
9
SQLVision.Core/Interfaces/ISqlScriptParser.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using SQLVision.Core.Models;
|
||||
|
||||
namespace SQLVision.Core.Interfaces;
|
||||
|
||||
public interface ISqlScriptParser
|
||||
{
|
||||
ScriptMetadata Parse(string filePath, string sqlContent);
|
||||
Task<ScriptMetadata> ParseAsync(string filePath, CancellationToken cancellationToken = default);
|
||||
}
|
||||
11
SQLVision.Core/Interfaces/ISqlVisionPlugin.cs
Normal file
@@ -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();
|
||||
}
|
||||
12
SQLVision.Core/Models/ChartSeries.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SQLVision.Core.Models;
|
||||
|
||||
public class ChartSeries
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<object> Values { get; set; } = new();
|
||||
public string Color { get; set; }
|
||||
public double LineSmoothness { get; set; } = 0;
|
||||
public bool ShowPoints { get; set; } = true;
|
||||
}
|
||||
36
SQLVision.Core/Models/ExecutionHistoryItem.cs
Normal file
@@ -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<string, object> 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; }
|
||||
}
|
||||
41
SQLVision.Core/Models/ExecutionResult.cs
Normal file
@@ -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<string, object> Parameters { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("executedSql")]
|
||||
public string ExecutedSql { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("rowCount")]
|
||||
public int RowCount { get; set; }
|
||||
|
||||
[JsonPropertyName("metrics")]
|
||||
public Dictionary<string, object> Metrics { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("connectionName")]
|
||||
public string? ConnectionName { get; set; }
|
||||
}
|
||||
|
||||
12
SQLVision.Core/Models/ExportOptions.cs
Normal file
@@ -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<string, object> CustomOptions { get; set; } = new();
|
||||
}
|
||||
38
SQLVision.Core/Models/OutputDefinition.cs
Normal file
@@ -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<string, string> 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;
|
||||
}
|
||||
8
SQLVision.Core/Models/ScriptCategory.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SQLVision.Core.Models;
|
||||
|
||||
public class ScriptCategory
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<ScriptMetadata> Scripts { get; set; } = new();
|
||||
public bool IsExpanded { get; set; } = true;
|
||||
}
|
||||
63
SQLVision.Core/Models/ScriptMetadata.cs
Normal file
@@ -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<ScriptParameter> Parameters { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("outputs")]
|
||||
public List<OutputDefinition> Outputs { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("lastModified")]
|
||||
public DateTime LastModified { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[JsonPropertyName("category")]
|
||||
public string? Category { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public List<string> 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;
|
||||
}
|
||||
66
SQLVision.Core/Models/ScriptParameter.cs
Normal file
@@ -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<string, object>? DependencyValues { get; set; }
|
||||
|
||||
[JsonPropertyName("validationRules")]
|
||||
public Dictionary<string, object>? 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;
|
||||
}
|
||||
}
|
||||
17
SQLVision.Core/SQLVision.Core.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
SQLVision.Services/Configuration/CacheOptions.cs
Normal file
@@ -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
|
||||
}
|
||||
13
SQLVision.Services/Configuration/DatabaseOptions.cs
Normal file
@@ -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<string, string> 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();
|
||||
}
|
||||
8
SQLVision.Services/Configuration/RetryOptions.cs
Normal file
@@ -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;
|
||||
}
|
||||
86
SQLVision.Services/Exporters/CsvExporter.cs
Normal file
@@ -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<CsvExporter> _logger;
|
||||
|
||||
public string FormatName => "CSV";
|
||||
|
||||
public CsvExporter(ILogger<CsvExporter> 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<byte[]> 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();
|
||||
}
|
||||
}
|
||||
139
SQLVision.Services/Exporters/ExcelExporter.cs
Normal file
@@ -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<ExcelExporter> _logger;
|
||||
|
||||
public string FormatName => "Excel";
|
||||
|
||||
public ExcelExporter(ILogger<ExcelExporter> 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<byte[]> 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
|
||||
};
|
||||
}
|
||||
|
||||
59
SQLVision.Services/Exporters/JsonExporter.cs
Normal file
@@ -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<JsonExporter> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public string FormatName => "JSON";
|
||||
|
||||
public JsonExporter(ILogger<JsonExporter> 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<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
|
||||
{
|
||||
var records = ConvertDataTableToList(data);
|
||||
var json = JsonSerializer.Serialize(records, _jsonOptions);
|
||||
return System.Text.Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
|
||||
private List<Dictionary<string, object>> ConvertDataTableToList(DataTable data)
|
||||
{
|
||||
var list = new List<Dictionary<string, object>>();
|
||||
|
||||
foreach (DataRow row in data.Rows)
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
dict[column.ColumnName] = row[column] ?? DBNull.Value;
|
||||
}
|
||||
|
||||
list.Add(dict);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
312
SQLVision.Services/Parsers/SqlScriptParser.cs
Normal file
@@ -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<SqlScriptParser> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public SqlScriptParser(ILogger<SqlScriptParser> 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<DatabaseProvider>(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<Dictionary<string, object>>(
|
||||
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<ScriptParameter>(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<ParameterType>(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<OutputDefinition>(value, _jsonOptions);
|
||||
}
|
||||
|
||||
// Старый формат: type:subtype "Description"
|
||||
var match = Regex.Match(value, @"(\w+)(?::(\w+))?\s+""([^""]+)""");
|
||||
|
||||
if (!match.Success) return null;
|
||||
|
||||
return new OutputDefinition
|
||||
{
|
||||
Type = Enum.TryParse<OutputType>(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<ScriptMetadata> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
SQLVision.Services/SQLVision.Services.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SQLVision.Core\SQLVision.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- MSSQL драйвер -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
|
||||
<!-- Экспорт в Excel -->
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
|
||||
<!-- Экспорт в CSV -->
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
|
||||
<!-- DI, конфигурация, кэширование -->
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
122
SQLVision.Services/Services/ExportService.cs
Normal file
@@ -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<ExportService> _logger;
|
||||
private readonly Dictionary<string, IExportHandler> _exportHandlers;
|
||||
|
||||
public ExportService(
|
||||
ILogger<ExportService> logger,
|
||||
IEnumerable<IExportHandler> 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<byte[]> 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"
|
||||
};
|
||||
}
|
||||
122
SQLVision.Services/Services/PluginManager.cs
Normal file
@@ -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<PluginManager> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly List<ISqlVisionPlugin> _plugins = new();
|
||||
|
||||
public PluginManager(ILogger<PluginManager> 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<ISqlVisionPlugin> GetPlugins() => _plugins.AsReadOnly();
|
||||
|
||||
public T? GetPlugin<T>() where T : ISqlVisionPlugin
|
||||
=> _plugins.OfType<T>().FirstOrDefault();
|
||||
|
||||
public async Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> 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<IConfiguration>();
|
||||
Logger = serviceProvider.GetRequiredService<ILogger<PluginManager>>();
|
||||
}
|
||||
|
||||
public Task ShowNotificationAsync(string message, NotificationType type)
|
||||
{
|
||||
// TODO: Реализовать показ уведомлений через UI
|
||||
Logger.LogInformation("Plugin notification ({Type}): {Message}", type, message);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
197
SQLVision.Services/Services/ScriptManager.cs
Normal file
@@ -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<ScriptManager> _logger;
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
private readonly ConcurrentDictionary<string, ScriptMetadata> _scripts;
|
||||
private readonly string _scriptsDirectory;
|
||||
|
||||
public event EventHandler<ScriptChangedEventArgs>? ScriptChanged;
|
||||
public event EventHandler<ScriptsReloadedEventArgs>? ScriptsReloaded;
|
||||
|
||||
public ScriptManager(ISqlScriptParser parser, IConfiguration configuration, ILogger<ScriptManager> logger)
|
||||
{
|
||||
_parser = parser;
|
||||
_logger = logger;
|
||||
_scripts = new ConcurrentDictionary<string, ScriptMetadata>();
|
||||
_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<IEnumerable<ScriptMetadata>> 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<ScriptMetadata>();
|
||||
}
|
||||
|
||||
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<ScriptMetadata?> 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<ScriptMetadata> 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<string> 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();
|
||||
}
|
||||
}
|
||||
36
SQLVision.Services/Services/ServiceExtensions.cs
Normal file
@@ -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<ISqlScriptParser, SqlScriptParser>();
|
||||
|
||||
// Регистрация сервиса выполнения SQL
|
||||
services.TryAddSingleton<ISqlExecutionService, SqlExecutionService>();
|
||||
|
||||
// Регистрация менеджера скриптов
|
||||
services.TryAddSingleton<IScriptManager, ScriptManager>();
|
||||
|
||||
// Регистрация сервиса экспорта
|
||||
services.TryAddSingleton<IExportService, ExportService>();
|
||||
|
||||
// Регистрация менеджера плагинов
|
||||
services.TryAddSingleton<IPluginManager, PluginManager>();
|
||||
|
||||
// Регистрация экспортеров
|
||||
services.TryAddSingleton<IExportHandler, ExcelExporter>();
|
||||
services.TryAddSingleton<IExportHandler, CsvExporter>();
|
||||
services.TryAddSingleton<IExportHandler, JsonExporter>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
305
SQLVision.Services/Services/SqlExecutionService.cs
Normal file
@@ -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<SqlExecutionService> _logger;
|
||||
private readonly IOptions<DatabaseOptions> _options;
|
||||
private readonly ConcurrentDictionary<string, Task<DataTable>> _loadingComboBoxData = new();
|
||||
|
||||
public SqlExecutionService(
|
||||
IMemoryCache cache,
|
||||
ILogger<SqlExecutionService> logger,
|
||||
IOptions<DatabaseOptions> options)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(
|
||||
ScriptMetadata script,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var result = new ExecutionResult
|
||||
{
|
||||
Parameters = new Dictionary<string, object>(parameters),
|
||||
ExecutionDate = DateTime.UtcNow,
|
||||
ConnectionName = script.ConnectionString
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Генерация ключа кэша
|
||||
var cacheKey = GenerateCacheKey(script, parameters);
|
||||
if (_options.Value.Cache.Enabled)
|
||||
{
|
||||
if (_cache.TryGetValue<ExecutionResult>(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<DataTable>().Sum(t => t.Rows.Count);
|
||||
result.Metrics = new Dictionary<string, object>
|
||||
{
|
||||
["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<DataSet> ExecuteQueryAsync(
|
||||
string sql,
|
||||
List<SqlParameter> 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<SqlParameter> Parameters) PrepareSql(
|
||||
string sql,
|
||||
Dictionary<string, object> parameters,
|
||||
DatabaseProvider provider)
|
||||
{
|
||||
var dbParameters = new List<SqlParameter>();
|
||||
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<ExecutionResult> ExecuteAsync(
|
||||
string sql,
|
||||
Dictionary<string, object> 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<bool> 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<DataTable> 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<string, object> 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();
|
||||
}
|
||||
}
|
||||
397
SQLVision.UI/Controls/ControlFactory.cs
Normal file
@@ -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<ControlFactory> _logger;
|
||||
|
||||
public ControlFactory(IServiceProvider serviceProvider, ILogger<ControlFactory> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public FrameworkElement CreateControl(ScriptParameter parameter, Action<object> 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<object> 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<object> 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<object> 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<object> 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<object> 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<ISqlExecutionService>();
|
||||
var result = await executionService.ExecuteAsync(
|
||||
parameter.TableQuery,
|
||||
new Dictionary<string, object>(),
|
||||
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<object> onValueChanged)
|
||||
{
|
||||
var listBox = new ListBox
|
||||
{
|
||||
Header = parameter.DisplayName,
|
||||
SelectionMode = ListViewSelectionMode.Multiple
|
||||
};
|
||||
|
||||
// Загрузка данных для ListBox
|
||||
// Аналогично ComboBox
|
||||
|
||||
listBox.SelectionChanged += (s, e) =>
|
||||
{
|
||||
var selectedValues = listBox.SelectedItems.Cast<DataRowView>()
|
||||
.Select(r => r[parameter.ValueMember])
|
||||
.ToList();
|
||||
|
||||
onValueChanged(selectedValues);
|
||||
};
|
||||
|
||||
return listBox;
|
||||
}
|
||||
|
||||
private FrameworkElement CreateColorPicker(ScriptParameter parameter, Action<object> 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<object> 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<object> 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<JsonElement>(textBox.Text);
|
||||
onValueChanged(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Игнорируем ошибки парсинга JSON
|
||||
}
|
||||
};
|
||||
|
||||
return textBox;
|
||||
}
|
||||
|
||||
private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action<object> 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<string, object> currentValues)
|
||||
{
|
||||
// Обновление состояния контрола на основе зависимостей
|
||||
if (!string.IsNullOrEmpty(parameter.DependsOn))
|
||||
{
|
||||
var isEnabled = CheckDependency(parameter, currentValues);
|
||||
control.IsEnabled = isEnabled;
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
// Сброс значения, если контрол отключен
|
||||
ResetControlValue(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckDependency(ScriptParameter parameter, Dictionary<string, object> 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<IConfiguration>();
|
||||
return configuration.GetConnectionString("Default") ??
|
||||
configuration["Database:DefaultConnection"];
|
||||
}
|
||||
}
|
||||
33
SQLVision.UI/SQLVision.UI.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>SQLVision.UI</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Все зависимости -->
|
||||
<ProjectReference Include="..\SQLVision.Services\SQLVision.Services.csproj" />
|
||||
<ProjectReference Include="..\SQLVision.Visualizers\SQLVision.Visualizers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.230913002" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
528
SQLVision.UI/ViewModels/MainViewModel.cs
Normal file
@@ -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<MainViewModel> _logger;
|
||||
|
||||
private readonly ObservableCollection<ScriptMetadata> _scripts = new();
|
||||
private readonly ObservableCollection<ScriptCategory> _scriptCategories = new();
|
||||
private readonly ObservableCollection<ExecutionHistoryItem> _history = new();
|
||||
private readonly ObservableCollection<ResultTabViewModel> _resultTabs = new();
|
||||
|
||||
private ScriptMetadata _selectedScript;
|
||||
private bool _isBusy;
|
||||
private string _statusMessage;
|
||||
private ResultTabViewModel _selectedResultTab;
|
||||
private string _searchText;
|
||||
|
||||
public ObservableCollection<ScriptCategory> ScriptCategories => _scriptCategories;
|
||||
public ObservableCollection<ExecutionHistoryItem> History => _history;
|
||||
public ObservableCollection<ResultTabViewModel> 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<ParameterViewModel> 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<MainViewModel> 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<ScriptMetadata>(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<string, object> 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<string, object>(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<string> { ".xlsx" });
|
||||
savePicker.FileTypeChoices.Add("CSV файл", new List<string> { ".csv" });
|
||||
savePicker.FileTypeChoices.Add("JSON файл", new List<string> { ".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<string, object> 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<string, object>();
|
||||
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<Dictionary<string, object>>(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);
|
||||
}
|
||||
}
|
||||
124
SQLVision.UI/ViewModels/ParameterViewModel.cs
Normal file
@@ -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<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
SQLVision.UI/ViewModels/ResultTabViewModel.cs
Normal file
@@ -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()
|
||||
{
|
||||
// Копирование данных в буфер обмена
|
||||
}
|
||||
}
|
||||
29
SQLVision.UI/ViewModels/ScriptCategory.cs
Normal file
@@ -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<ScriptMetadata> _scripts;
|
||||
private bool _isExpanded = true;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<ScriptMetadata> Scripts
|
||||
{
|
||||
get => _scripts;
|
||||
set => SetProperty(ref _scripts, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
}
|
||||
359
SQLVision.UI/Views/MainWindow.xaml
Normal file
@@ -0,0 +1,359 @@
|
||||
<Window
|
||||
x:Class="SQLVision.UI.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="using:SQLVision.UI.Controls"
|
||||
xmlns:charts="using:LiveChartsCore.SkiaSharpView.WinUI"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:converters="using:SQLVision.UI.Converters"
|
||||
mc:Ignorable="d"
|
||||
Title="SQLVision"
|
||||
Width="1200"
|
||||
Height="800"
|
||||
MinWidth="800"
|
||||
MinHeight="600">
|
||||
|
||||
<Window.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
|
||||
<converters:InverseBooleanConverter x:Key="InverseBool"/>
|
||||
|
||||
<Style TargetType="Button" x:Key="IconButtonStyle">
|
||||
<Setter Property="Margin" Value="2"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="MinWidth" Value="80"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Панель инструментов -->
|
||||
<CommandBar Grid.Row="0" DefaultLabelPosition="Right">
|
||||
<AppBarButton
|
||||
Icon="Refresh"
|
||||
Label="Обновить скрипты"
|
||||
Command="{x:Bind ViewModel.LoadScriptsCommand}"/>
|
||||
<AppBarSeparator/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="Play"
|
||||
Label="Выполнить"
|
||||
Command="{x:Bind ViewModel.ExecuteCommand}"
|
||||
IsEnabled="{x:Bind ViewModel.ExecuteCommand.IsRunning, Converter={StaticResource InverseBool}, Mode=OneWay}"/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="Save"
|
||||
Label="Экспорт"
|
||||
Command="{x:Bind ViewModel.ExportCommand}"/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="Copy"
|
||||
Label="Копировать SQL"
|
||||
Command="{x:Bind ViewModel.CopySqlCommand}"/>
|
||||
|
||||
<AppBarSeparator/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="Clear"
|
||||
Label="Очистить результаты"
|
||||
Command="{x:Bind ViewModel.ClearResultsCommand}"/>
|
||||
|
||||
<AppBarSeparator/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="SaveLocal"
|
||||
Label="Сохранить параметры"
|
||||
Command="{x:Bind ViewModel.SaveParametersCommand}"/>
|
||||
|
||||
<AppBarButton
|
||||
Icon="OpenLocal"
|
||||
Label="Загрузить параметры"
|
||||
Command="{x:Bind ViewModel.LoadParametersCommand}"/>
|
||||
|
||||
<CommandBar.Content>
|
||||
<TextBlock
|
||||
Text="SQLVision"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Margin="12,0"/>
|
||||
</CommandBar.Content>
|
||||
</CommandBar>
|
||||
|
||||
<!-- Основное содержимое -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" MinWidth="250"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Левая панель: скрипты и параметры -->
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Поиск -->
|
||||
<AutoSuggestBox
|
||||
Grid.Row="0"
|
||||
PlaceholderText="Поиск скриптов..."
|
||||
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="8">
|
||||
<AutoSuggestBox.QueryIcon>
|
||||
<SymbolIcon Symbol="Find"/>
|
||||
</AutoSuggestBox.QueryIcon>
|
||||
</AutoSuggestBox>
|
||||
|
||||
<!-- Дерево скриптов -->
|
||||
<TreeView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{x:Bind ViewModel.ScriptCategories, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedScript, Mode=TwoWay}"
|
||||
Margin="8">
|
||||
|
||||
<TreeView.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ScriptCategory">
|
||||
<TreeViewItem
|
||||
ItemsSource="{x:Bind Scripts}"
|
||||
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}">
|
||||
|
||||
<TreeViewItem.Header>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{x:Bind Name}" FontWeight="SemiBold"/>
|
||||
<TextBlock
|
||||
Text="{x:Bind Scripts.Count}"
|
||||
Foreground="Gray"
|
||||
FontSize="12"/>
|
||||
</StackPanel>
|
||||
</TreeViewItem.Header>
|
||||
|
||||
<TreeViewItem.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:ScriptMetadata">
|
||||
<TreeViewItem>
|
||||
<StackPanel Orientation="Vertical" Spacing="2">
|
||||
<TextBlock
|
||||
Text="{x:Bind FileName}"
|
||||
TextWrapping="Wrap"
|
||||
FontWeight="Normal"/>
|
||||
<TextBlock
|
||||
Text="{x:Bind Description}"
|
||||
FontSize="11"
|
||||
Foreground="Gray"
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
|
||||
<!-- Параметры скрипта -->
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="8"
|
||||
MaxHeight="400">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
x:Name="ParametersPanel"
|
||||
Spacing="12"
|
||||
Visibility="{x:Bind ViewModel.SelectedScript, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}">
|
||||
|
||||
<TextBlock
|
||||
Text="Параметры"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Parameters, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ParameterViewModel">
|
||||
<StackPanel Spacing="4" Margin="0,0,0,8">
|
||||
<ContentPresenter Content="{x:Bind Control}"/>
|
||||
|
||||
<TextBlock
|
||||
Text="{x:Bind ValidationError}"
|
||||
Foreground="Red"
|
||||
FontSize="11"
|
||||
Visibility="{x:Bind HasError, Converter={StaticResource BoolToVisibility}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Splitter -->
|
||||
<GridSplitter
|
||||
Grid.Column="1"
|
||||
Width="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="Transparent"/>
|
||||
|
||||
<!-- Правая панель: результаты -->
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Вкладки с результатами -->
|
||||
<muxc:TabView
|
||||
Grid.Row="0"
|
||||
ItemsSource="{x:Bind ViewModel.ResultTabs, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedResultTab, Mode=TwoWay}"
|
||||
TabWidthMode="SizeToContent"
|
||||
CanReorderTabs="True"
|
||||
CanCloseTabs="True"
|
||||
TabCloseRequested="OnTabCloseRequested">
|
||||
|
||||
<muxc:TabView.TabItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ResultTabViewModel">
|
||||
<muxc:TabViewItem
|
||||
Header="{x:Bind Title}"
|
||||
IconSource="{x:Bind IconSource}">
|
||||
|
||||
<ScrollViewer
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ContentPresenter Content="{x:Bind Content}"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<muxc:TabViewItem.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
Text="Экспорт"
|
||||
Command="{x:Bind ExportCommand}"
|
||||
Icon="Save"/>
|
||||
<MenuFlyoutItem
|
||||
Text="Копировать данные"
|
||||
Command="{x:Bind CopyDataCommand}"
|
||||
Icon="Copy"/>
|
||||
<MenuFlyoutSeparator/>
|
||||
<MenuFlyoutItem
|
||||
Text="Закрыть"
|
||||
Click="OnCloseTabClick"/>
|
||||
</MenuFlyout>
|
||||
</muxc:TabViewItem.ContextFlyout>
|
||||
</muxc:TabViewItem>
|
||||
</DataTemplate>
|
||||
</muxc:TabView.TabItemTemplate>
|
||||
</muxc:TabView>
|
||||
|
||||
<!-- Панель истории -->
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Visibility="{x:Bind ViewModel.History.Count, Converter={StaticResource CountToVisibilityConverter}, Mode=OneWay}">
|
||||
|
||||
<Expander
|
||||
Header="История выполненных запросов"
|
||||
IsExpanded="False"
|
||||
Margin="8">
|
||||
|
||||
<ListView
|
||||
ItemsSource="{x:Bind ViewModel.History, Mode=OneWay}"
|
||||
MaxHeight="200"
|
||||
SelectionMode="None">
|
||||
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:ExecutionHistoryItem">
|
||||
<Grid Padding="8" BorderBrush="LightGray" BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock
|
||||
Text="{x:Bind ScriptName}"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock
|
||||
Text="{x:Bind ExecutionTime, StringFormat='{}{0:dd.MM.yyyy HH:mm:ss}'}"
|
||||
FontSize="11"
|
||||
Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Text="{x:Bind Duration, StringFormat='{}{0:mm\\:ss}'}"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Text="{x:Bind RowCount, StringFormat='{}Строк: {0}'}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Expander>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Статус бар -->
|
||||
<StatusBar Grid.Row="2">
|
||||
<StatusBarItem>
|
||||
<ProgressRing
|
||||
Width="16"
|
||||
Height="16"
|
||||
IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}"/>
|
||||
</StatusBarItem>
|
||||
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{x:Bind ViewModel.StatusMessage, Mode=OneWay}"/>
|
||||
</StatusBarItem>
|
||||
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
<TextBlock>
|
||||
<Run Text="Скриптов:"/>
|
||||
<Run Text="{x:Bind ViewModel.ScriptCategories.Sum(c => c.Scripts.Count), Mode=OneWay}"/>
|
||||
<Run Text="|"/>
|
||||
<Run Text="Вкладок:"/>
|
||||
<Run Text="{x:Bind ViewModel.ResultTabs.Count, Mode=OneWay}"/>
|
||||
</TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
|
||||
<!-- Прогресс выполнения -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Background="#CC000000"
|
||||
Visibility="{x:Bind ViewModel.IsBusy, Converter={StaticResource BoolToVisibility}, Mode=OneWay}">
|
||||
|
||||
<Border
|
||||
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<StackPanel Spacing="16" HorizontalAlignment="Center">
|
||||
<ProgressRing Width="40" Height="40" IsActive="True"/>
|
||||
<TextBlock
|
||||
Text="Выполнение запроса..."
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
169
SQLVision.UI/Views/MainWindow.xaml.cs
Normal file
@@ -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<string, object> CollectParameterValues()
|
||||
{
|
||||
var values = new Dictionary<string, object>();
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
599
SQLVision.Visualizers/Factories/ControlFactory.cs
Normal file
@@ -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<ControlFactory> _logger;
|
||||
private readonly ISqlExecutionService? _executionService;
|
||||
|
||||
public ControlFactory(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<ControlFactory> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
|
||||
_executionService = serviceProvider.GetService<ISqlExecutionService>();
|
||||
}
|
||||
|
||||
public FrameworkElement CreateControl(ScriptParameter parameter, Action<object?> 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<object?> 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<object?> 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<object?> 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<object?> 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<object?> 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<string> { "Ошибка загрузки данных" };
|
||||
comboBox.IsEnabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private FrameworkElement CreateListView(ScriptParameter parameter, Action<object?> 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<object>();
|
||||
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<object?> 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<object?> 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<object?> 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<JsonElement>(textBox.Text);
|
||||
onValueChanged(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Игнорируем ошибки парсинга JSON
|
||||
}
|
||||
};
|
||||
|
||||
stackPanel.Children.Add(header);
|
||||
stackPanel.Children.Add(textBox);
|
||||
|
||||
return stackPanel;
|
||||
}
|
||||
|
||||
private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action<object?> 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<string, object?> currentValues)
|
||||
{
|
||||
// Обновление состояния контрола на основе зависимостей
|
||||
if (!string.IsNullOrEmpty(parameter.DependsOn))
|
||||
{
|
||||
var isEnabled = CheckDependency(parameter, currentValues);
|
||||
control.IsEnabled = isEnabled;
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
ResetControlValue(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckDependency(ScriptParameter parameter, Dictionary<string, object?> 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<TextBox>(control) ??
|
||||
FindChildOfType<ComboBox>(control) ??
|
||||
FindChildOfType<CheckBox>(control) ??
|
||||
FindChildOfType<DatePicker>(control) ??
|
||||
FindChildOfType<NumberBox>(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<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
var queue = new Queue<DependencyObject>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
40
SQLVision.Visualizers/Factories/VisualizerFactory.cs
Normal file
@@ -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<OutputType, IVisualizer> _visualizers;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public VisualizerFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_visualizers = new Dictionary<OutputType, IVisualizer>
|
||||
{
|
||||
[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;
|
||||
}
|
||||
}
|
||||
10
SQLVision.Visualizers/Interfaces/IControlFactory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using SQLVision.Core.Models;
|
||||
|
||||
namespace SQLVision.Visualizers.Interfaces;
|
||||
|
||||
public interface IControlFactory
|
||||
{
|
||||
FrameworkElement CreateControl(ScriptParameter parameter, Action<object?> onValueChanged);
|
||||
void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary<string, object?> currentValues);
|
||||
}
|
||||
12
SQLVision.Visualizers/Interfaces/IVisualizer.cs
Normal file
@@ -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);
|
||||
}
|
||||
9
SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs
Normal file
@@ -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);
|
||||
}
|
||||
25
SQLVision.Visualizers/SQLVision.Visualizers.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SQLVision.Core\SQLVision.Core.csproj" />
|
||||
<ProjectReference Include="..\SQLVision.Services\SQLVision.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- WinUI 3 -->
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
|
||||
<!-- Графики -->
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
SQLVision.Visualizers/ServiceExtensions.cs
Normal file
@@ -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<IControlFactory, ControlFactory>();
|
||||
services.AddSingleton<IVisualizerFactory, VisualizerFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
184
SQLVision.Visualizers/Visualizers/ChartVisualizer.cs
Normal file
@@ -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<ISeries>();
|
||||
|
||||
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<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name,
|
||||
Fill = null,
|
||||
GeometrySize = 8,
|
||||
LineSmoothness = 0
|
||||
},
|
||||
ChartType.Bar => new ColumnSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name
|
||||
},
|
||||
ChartType.Area => new LineSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name,
|
||||
Fill = new SolidColorPaint(SKColors.Blue.WithAlpha(50))
|
||||
},
|
||||
ChartType.Scatter => new ScatterSeries<ObservablePoint>
|
||||
{
|
||||
Values = values.Select((v, i) => new ObservablePoint(i, v)),
|
||||
Name = name,
|
||||
GeometrySize = 10
|
||||
},
|
||||
_ => new LineSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Axis[] CreateXAxes(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
var labels = new List<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
75
SQLVision.Visualizers/Visualizers/TableVisualizer.cs
Normal file
@@ -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;
|
||||
}
|
||||
70
SQLVision.Visualizers/Visualizers/TextVisualizer.cs
Normal file
@@ -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;
|
||||
}
|
||||
26
SQLVision.Visualizers/app.manifest
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="SQLVision.Application"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
20
SQLVision.slnx
Normal file
@@ -0,0 +1,20 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="ARM64" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/Элементы решения/" />
|
||||
<Project Path="SQLVision.Core/SQLVision.Core.csproj" />
|
||||
<Project Path="SQLVision.Services/SQLVision.Services.csproj" />
|
||||
<Project Path="SQLVision.UI/SQLVision.UI.csproj">
|
||||
<Platform Solution="Debug|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="SQLVision.Visualizers/SQLVision.Visualizers.csproj" />
|
||||
<Project Path="SQLVision/SQLVision.csproj" Id="ad5352de-2482-4124-8033-4e873ae1d181">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Platform Solution="*|x86" Project="x86" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Solution>
|
||||
29
SQLVision/App.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="SQLVision.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converts="using:SQLVision.Converters">
|
||||
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Fluent Icons -->
|
||||
<FontFamily x:Key="FluentIconsFont">Segoe Fluent Icons</FontFamily>
|
||||
|
||||
<!-- Converters -->
|
||||
<converts:ParameterTypeToVisibilityConverter x:Key="ParameterTypeToVisibilityConverter"/>
|
||||
<converts:OutputTypeToVisibilityConverter x:Key="OutputTypeToVisibilityConverter"/>
|
||||
<converts:ObjectToDateTimeOffsetConverter x:Key="ObjectToDateTimeOffsetConverter"/>
|
||||
<converts:ObjectToNullableBoolConverter x:Key="ObjectToNullableBoolConverter"/>
|
||||
<converts:DataTableToEnumerableConverter x:Key="DataTableToEnumerableConverter"/>
|
||||
<converts:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
35
SQLVision/App.xaml.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
/// <summary>
|
||||
/// 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().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
SQLVision/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
SQLVision/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
SQLVision/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
SQLVision/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
SQLVision/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
SQLVision/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
14
SQLVision/Converters/BoolToVisibilityConverter.cs
Normal file
@@ -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();
|
||||
}
|
||||
34
SQLVision/Converters/DataTableToEnumerableConverter.cs
Normal file
@@ -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<Dictionary<string, object>>();
|
||||
|
||||
foreach (DataRow row in dataTable.Rows)
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
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();
|
||||
}
|
||||
23
SQLVision/Converters/ObjectToDateTimeOffsetConverter.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
34
SQLVision/Converters/ObjectToNullableBoolConverter.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
24
SQLVision/Converters/OutputTypeToVisibilityConverter.cs
Normal file
@@ -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();
|
||||
}
|
||||
27
SQLVision/Converters/ParameterTypeToVisibilityConverter.cs
Normal file
@@ -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();
|
||||
}
|
||||
9
SQLVision/Enums/OutputType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SQLVision.Enums;
|
||||
|
||||
public enum OutputType
|
||||
{
|
||||
Table,
|
||||
ChartLine,
|
||||
ChartBar,
|
||||
ChartPie
|
||||
}
|
||||
10
SQLVision/Enums/ParameterType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SQLVision.Enums;
|
||||
|
||||
public enum ParameterType
|
||||
{
|
||||
Int,
|
||||
String,
|
||||
DateTime,
|
||||
Bool,
|
||||
Table
|
||||
}
|
||||
12
SQLVision/Models/ConnectionProfile.cs
Normal file
@@ -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; } = "";
|
||||
}
|
||||
12
SQLVision/Models/ExecutionResult.cs
Normal file
@@ -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<DataTable> Tables { get; init; } = Array.Empty<DataTable>();
|
||||
}
|
||||
9
SQLVision/Models/OutputDefinition.cs
Normal file
@@ -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;
|
||||
}
|
||||
13
SQLVision/Models/ParameterDefinition.cs
Normal file
@@ -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; } // имя другого параметра
|
||||
}
|
||||
9
SQLVision/Models/ParameterValueSet.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SQLVision.Models;
|
||||
|
||||
public sealed class ParameterValueSet
|
||||
{
|
||||
public string ScriptFilePath { get; set; } = string.Empty;
|
||||
public Dictionary<string, object?> Values { get; set; } = new();
|
||||
}
|
||||
15
SQLVision/Models/ScriptMetadata.cs
Normal file
@@ -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<ParameterDefinition> Parameters { get; init; } =
|
||||
Array.Empty<ParameterDefinition>();
|
||||
public IReadOnlyList<OutputDefinition> Outputs { get; init; } =
|
||||
Array.Empty<OutputDefinition>();
|
||||
}
|
||||
13
SQLVision/Models/ScriptTreeNode.cs
Normal file
@@ -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<ScriptTreeNode> Children { get; } = new();
|
||||
|
||||
public bool IsFolder => FilePath is null;
|
||||
}
|
||||
51
SQLVision/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="79ca160c-7108-46fa-9aa2-2ba97d8d499e"
|
||||
Publisher="CN=frost"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="79ca160c-7108-46fa-9aa2-2ba97d8d499e" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>SQLVision</DisplayName>
|
||||
<PublisherDisplayName>frost</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="SQLVision"
|
||||
Description="SQLVision"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
10
SQLVision/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"SQLVision (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
},
|
||||
"SQLVision (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
SQLVision/SQLVision.csproj
Normal file
@@ -0,0 +1,62 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>SQLVision</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Nullable>enable</Nullable>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Views\ConnectionPage.xaml" />
|
||||
<None Remove="Views\HelpWindow.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc2" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Scripts\ServerInfo.sql">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Page Update="Views\ConnectionPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Views\HelpWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
63
SQLVision/Scripts/ServerInfo.sql
Normal file
@@ -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
|
||||
14
SQLVision/Selectors/OutputTemplateSelector.cs
Normal file
@@ -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!;
|
||||
}
|
||||
}
|
||||
42
SQLVision/Services/ConnectionStorageService.cs
Normal file
@@ -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<ConnectionProfile> Load()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
return new List<ConnectionProfile>();
|
||||
|
||||
var json = File.ReadAllText(_filePath);
|
||||
return JsonSerializer.Deserialize<List<ConnectionProfile>>(json) ?? new();
|
||||
}
|
||||
|
||||
public void Save(List<ConnectionProfile> profiles)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(profiles, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
}
|
||||
27
SQLVision/Services/ExcelExportService.cs
Normal file
@@ -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<DataTable> 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);
|
||||
}
|
||||
}
|
||||
42
SQLVision/Services/ParameterResolver.cs
Normal file
@@ -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<string, object?> parameterValues,
|
||||
IReadOnlyList<Models.ParameterDefinition> 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("'", "''")}'"
|
||||
};
|
||||
}
|
||||
}
|
||||
106
SQLVision/Services/SqlCommentParser.cs
Normal file
@@ -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<ParameterDefinition>();
|
||||
var outputs = new List<OutputDefinition>();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
63
SQLVision/Services/SqlExecutor.cs
Normal file
@@ -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<ExecutionResult> ExecuteAsync(
|
||||
ScriptMetadata metadata,
|
||||
string sqlText,
|
||||
IReadOnlyDictionary<ParameterDefinition, object?> parameterValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Здесь sqlText уже с параметрами вида @ParamName
|
||||
var dataTables = new List<DataTable>();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
30
SQLVision/Services/TableDataProvider.cs
Normal file
@@ -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<DataTable> 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;
|
||||
}
|
||||
}
|
||||
23
SQLVision/ViewModels/BaseViewModel.cs
Normal file
@@ -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<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
RaisePropertyChanged(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
109
SQLVision/ViewModels/ConnectionViewModel.cs
Normal file
@@ -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<string> databases = new();
|
||||
[ObservableProperty] private string selectedDatabase = "";
|
||||
[ObservableProperty] private string statusMessage = "";
|
||||
|
||||
public bool IsSqlAuth => AuthType == "SQL Server";
|
||||
|
||||
public event Action<ConnectionProfile>? ConnectionSaved;
|
||||
private readonly ConnectionStorageService _storage;
|
||||
private List<ConnectionProfile> _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;
|
||||
}
|
||||
}
|
||||
339
SQLVision/ViewModels/MainViewModel.cs
Normal file
@@ -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<ConnectionProfile> 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<ScriptMetadata> Scripts { get; } = new();
|
||||
|
||||
// Дерево скриптов для TreeView
|
||||
public ObservableCollection<ScriptTreeNode> 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<ParameterViewModel> Parameters { get; } = new();
|
||||
public ObservableCollection<OutputViewModel> 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<ParameterDefinition, object?> 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);
|
||||
}
|
||||
}
|
||||
16
SQLVision/ViewModels/OutputViewModel.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
46
SQLVision/ViewModels/ParameterViewModel.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
24
SQLVision/ViewModels/RelayCommand.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SQLVision.ViewModels;
|
||||
|
||||
public sealed class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? 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);
|
||||
}
|
||||
80
SQLVision/Views/ConnectionPage.xaml
Normal file
@@ -0,0 +1,80 @@
|
||||
<Page
|
||||
x:Class="SQLVision.Views.ConnectionPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
Padding="24">
|
||||
|
||||
<StackPanel Spacing="20" Width="420">
|
||||
|
||||
<!-- HEADER -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<FontIcon FontFamily="{StaticResource FluentIconsFont}"
|
||||
Glyph=""
|
||||
FontSize="28"/>
|
||||
<TextBlock Text="Подключение к SQL Server"
|
||||
Style="{StaticResource VS.HeaderText}"
|
||||
FontSize="26"
|
||||
Margin="0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Сервер -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Сервер" FontWeight="SemiBold"/>
|
||||
<TextBox Text="{Binding ServerName, Mode=TwoWay}"
|
||||
PlaceholderText="SERVER\\INSTANCE или hostname"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Тип аутентификации -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Аутентификация" FontWeight="SemiBold"/>
|
||||
<ComboBox SelectedItem="{Binding AuthType, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Windows"/>
|
||||
<ComboBoxItem Content="SQL Server"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- SQL Login -->
|
||||
<StackPanel Spacing="4"
|
||||
Visibility="{Binding IsSqlAuth, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
|
||||
<TextBlock Text="Логин" FontWeight="SemiBold"/>
|
||||
<TextBox Text="{Binding Login, Mode=TwoWay}"
|
||||
PlaceholderText="sa или другой пользователь"/>
|
||||
|
||||
<TextBlock Text="Пароль" FontWeight="SemiBold"/>
|
||||
<PasswordBox Password="{Binding Password, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- База данных -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="База данных" FontWeight="SemiBold"/>
|
||||
<ComboBox ItemsSource="{Binding Databases}"
|
||||
SelectedItem="{Binding SelectedDatabase, Mode=TwoWay}"
|
||||
PlaceholderText="Выберите базу"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,12,0,0">
|
||||
<Button Content="Проверить"
|
||||
Command="{Binding TestConnectionCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}"/>
|
||||
|
||||
<Button Content="Сохранить"
|
||||
Command="{Binding SaveCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Статус -->
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
Foreground="DarkGreen"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
31
SQLVision/Views/ConnectionPage.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace SQLVision.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class ConnectionPage : Page
|
||||
{
|
||||
public ConnectionPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||