Добавьте файлы проекта.
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||