diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e233f60..325e626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,15 @@ jobs: with: dotnet-version: 8.0.x + - name: Install WASM workload + run: dotnet workload install wasm-tools + - name: Restore dependencies run: | dotnet restore src/PlanViewer.Core/PlanViewer.Core.csproj dotnet restore src/PlanViewer.App/PlanViewer.App.csproj dotnet restore src/PlanViewer.Cli/PlanViewer.Cli.csproj + dotnet restore src/PlanViewer.Web/PlanViewer.Web.csproj dotnet restore tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj - name: Build all projects @@ -30,6 +34,7 @@ jobs: dotnet build src/PlanViewer.Core/PlanViewer.Core.csproj -c Release --no-restore dotnet build src/PlanViewer.App/PlanViewer.App.csproj -c Release --no-restore dotnet build src/PlanViewer.Cli/PlanViewer.Cli.csproj -c Release --no-restore + dotnet build src/PlanViewer.Web/PlanViewer.Web.csproj -c Release --no-restore dotnet build tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-restore - name: Run tests diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..412d720 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,58 @@ +name: Deploy Web App + +on: + push: + branches: [main] + paths: + - 'src/PlanViewer.Core/**' + - 'src/PlanViewer.Web/**' + - '.github/workflows/deploy-web.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install WASM workload + run: dotnet workload install wasm-tools + + - name: Publish Blazor WASM + run: dotnet publish src/PlanViewer.Web/PlanViewer.Web.csproj -c Release -o publish + + - name: Add .nojekyll + run: touch publish/wwwroot/.nojekyll + + - name: Add CNAME + run: echo 'plans.erikdarling.com' > publish/wwwroot/CNAME + + - name: Add 404 fallback + run: cp publish/wwwroot/index.html publish/wwwroot/404.html + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: publish/wwwroot + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/PlanViewer.sln b/PlanViewer.sln index 2f57790..685bc60 100644 --- a/PlanViewer.sln +++ b/PlanViewer.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanViewer.App", "src\PlanV EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanViewer.Cli", "src\PlanViewer.Cli\PlanViewer.Cli.csproj", "{1504CE29-3CBF-4F0B-A46E-54644946B8ED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanViewer.Web", "src\PlanViewer.Web\PlanViewer.Web.csproj", "{B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A06217BE-DBE2-47D0-BD59-93F2108D447C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanViewer.Core.Tests", "tests\PlanViewer.Core.Tests\PlanViewer.Core.Tests.csproj", "{399A69AD-0CD1-4E9B-9988-E94882B827E6}" @@ -40,11 +42,16 @@ Global {399A69AD-0CD1-4E9B-9988-E94882B827E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {399A69AD-0CD1-4E9B-9988-E94882B827E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {399A69AD-0CD1-4E9B-9988-E94882B827E6}.Release|Any CPU.Build.0 = Release|Any CPU + {B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {8904045D-E083-4010-A320-104B7466C044} = {21F75D2E-F228-49B3-825F-43F621760061} {F1018F88-B289-40CE-852A-56478DFBA91E} = {21F75D2E-F228-49B3-825F-43F621760061} {1504CE29-3CBF-4F0B-A46E-54644946B8ED} = {21F75D2E-F228-49B3-825F-43F621760061} + {B2D3F7A1-8C4E-4F5A-9D6B-1E2F3A4B5C6D} = {21F75D2E-F228-49B3-825F-43F621760061} {399A69AD-0CD1-4E9B-9988-E94882B827E6} = {A06217BE-DBE2-47D0-BD59-93F2108D447C} EndGlobalSection EndGlobal diff --git a/src/PlanViewer.Web/App.razor b/src/PlanViewer.Web/App.razor new file mode 100644 index 0000000..bf076d1 --- /dev/null +++ b/src/PlanViewer.Web/App.razor @@ -0,0 +1,10 @@ + + + + + + +

Page not found.

+
+
+
diff --git a/src/PlanViewer.Web/Layout/MainLayout.razor b/src/PlanViewer.Web/Layout/MainLayout.razor new file mode 100644 index 0000000..3981503 --- /dev/null +++ b/src/PlanViewer.Web/Layout/MainLayout.razor @@ -0,0 +1,26 @@ +@inherits LayoutComponentBase + +
+
+ + + Performance Studio +
+
+ +
+ + + @Body + + +

Something went wrong. Please refresh the page and try again.

+
+
+
+ + diff --git a/src/PlanViewer.Web/Pages/Index.razor b/src/PlanViewer.Web/Pages/Index.razor new file mode 100644 index 0000000..8e1cfd4 --- /dev/null +++ b/src/PlanViewer.Web/Pages/Index.razor @@ -0,0 +1,628 @@ +@page "/" + +@if (result == null) +{ +
+
+

Execution Plan Analysis

+

Paste or upload a .sqlplan file. Your plan XML never leaves your browser.

+ + @if (errorMessage != null) + { +
@errorMessage
+ } + +
+
+ + +
+ + @if (activeTab == "paste") + { + + + } + else + { +
+ +

Accepts .sqlplan and .xml files up to 10 MB

+
+ } +
+
+
+} +else +{ + @if (errorMessage != null) + { +
@errorMessage
+ } + +
+ + @sourceLabel + + @(result.Summary.HasActualStats ? "Actual Plan" : "Estimated Plan") + + @if (result.SqlServerBuild != null) + { + @result.SqlServerBuild + } +
+ + @* Statement selector for multi-statement plans *@ + @if (result.Statements.Count > 1) + { +
+ @for (int si = 0; si < result.Statements.Count; si++) + { + var idx = si; + var isActive = idx == activeStatement; + + } +
+ } + + @* Insights panel — horizontal cards matching desktop layout *@ +
+ + @* Runtime Summary *@ +
+

Runtime

+
+
+ Cost + @ActiveStmt!.EstimatedCost.ToString("N2") +
+ @if (ActiveStmt!.QueryTime != null) + { +
+ Elapsed + @ActiveStmt!.QueryTime.ElapsedTimeMs.ToString("N0") ms +
+
+ CPU + @ActiveStmt!.QueryTime.CpuTimeMs.ToString("N0") ms +
+ } + @if (ActiveStmt!.DegreeOfParallelism > 0) + { +
+ DOP + @ActiveStmt!.DegreeOfParallelism +
+ } + @if (ActiveStmt!.NonParallelReason != null) + { +
+ Serial + @ActiveStmt!.NonParallelReason +
+ } + @if (ActiveStmt!.MemoryGrant != null && ActiveStmt!.MemoryGrant.GrantedKB > 0) + { + var pctUsed = (double)ActiveStmt!.MemoryGrant.MaxUsedKB / ActiveStmt!.MemoryGrant.GrantedKB * 100; + var effClass = pctUsed >= 80 ? "eff-good" : pctUsed >= 60 ? "eff-ok" : pctUsed >= 40 ? "eff-warn" : "eff-bad"; +
+ Memory + @FormatKB(ActiveStmt!.MemoryGrant.GrantedKB) granted +
+
+ Used + @FormatKB(ActiveStmt!.MemoryGrant.MaxUsedKB) (@pctUsed.ToString("N0")%) +
+ } + @if (ActiveStmt!.OptimizationLevel != null) + { +
+ Optimization + @ActiveStmt!.OptimizationLevel +
+ } + @if (ActiveStmt!.CardinalityEstimationModel > 0) + { +
+ CE Model + @ActiveStmt!.CardinalityEstimationModel +
+ } +
+
+ + @* Missing Indexes *@ +
+

Missing Indexes @ActiveStmt!.MissingIndexes.Count

+
+ @if (ActiveStmt!.MissingIndexes.Count > 0) + { + @foreach (var mi in ActiveStmt!.MissingIndexes) + { +
+
@mi.Table
+
Impact: @mi.Impact.ToString("F0")%
+
@mi.CreateStatement
+
+ } + } + else + { +
No missing index suggestions
+ } +
+
+ + @* Parameters *@ +
+

Parameters @ActiveStmt!.Parameters.Count

+
+ @if (ActiveStmt!.Parameters.Count > 0) + { + + + + + + + @if (ActiveStmt!.Parameters.Any(p => p.RuntimeValue != null)) + { + + } + + + + @foreach (var p in ActiveStmt!.Parameters) + { + + + + + @if (ActiveStmt!.Parameters.Any(pp => pp.RuntimeValue != null)) + { + + } + + } + +
NameTypeCompiledRuntime
@p.Name@p.DataType@(p.CompiledValue ?? "?")@(p.RuntimeValue ?? "")
+ } + else + { +
No parameters
+ } +
+
+ + @* Wait Stats *@ +
+

Wait Stats + @if (ActiveStmt!.WaitStats.Count > 0) + { + @ActiveStmt!.WaitStats.Sum(w => w.WaitTimeMs).ToString("N0") ms + } +

+
+ @if (ActiveStmt!.WaitStats.Count > 0) + { + var maxWait = ActiveStmt!.WaitStats.Max(w => w.WaitTimeMs); + @foreach (var w in ActiveStmt!.WaitStats.OrderByDescending(w => w.WaitTimeMs)) + { + var barPct = maxWait > 0 ? (double)w.WaitTimeMs / maxWait * 100 : 0; +
+ @w.WaitType +
+
+
+ @w.WaitTimeMs.ToString("N0") ms +
+ } + } + else + { +
@(result.Summary.HasActualStats ? "No waits recorded" : "Estimated plan — no wait stats")
+ } +
+
+
+ + @* Warnings strip *@ + @if (GetAllWarnings(ActiveStmt!).Count > 0) + { +
+

Warnings @GetAllWarnings(ActiveStmt!).Count

+
+ @foreach (var w in GetAllWarnings(ActiveStmt!)) + { +
+ @w.Severity + @if (w.Operator != null) + { + @w.Operator + } + @w.Type + @w.Message +
+ } +
+
+ } + + @* Statement text *@ +
+ Query Text +
@ActiveStmt!.StatementText
+
+ + @* Plan tree *@ + @if (ActiveStmtPlan?.RootNode != null) + { + var extents = PlanLayoutEngine.GetExtents(ActiveStmtPlan!.RootNode); +
+
+ + @RenderConnectors(ActiveStmtPlan!.RootNode) + + @RenderPlanNodes(ActiveStmtPlan!.RootNode, true) +
+
+ } + + @* Full text output *@ +
+ Full Text Analysis +
@textOutput
+
+} + +@code { + private string activeTab = "paste"; + private string planXml = ""; + private string? errorMessage; + private bool isAnalyzing; + private AnalysisResult? result; + private ParsedPlan? parsedPlan; + private string? textOutput; + private string? sourceLabel; + private int activeStatement = 0; + + private StatementResult? ActiveStmt => result?.Statements.ElementAtOrDefault(activeStatement); + private PlanStatement? ActiveStmtPlan => parsedPlan?.Batches.SelectMany(b => b.Statements).ElementAtOrDefault(activeStatement); + + private async Task AnalyzePasted() + { + if (string.IsNullOrWhiteSpace(planXml)) + { + errorMessage = "Paste plan XML first."; + return; + } + + await RunAnalysis(planXml, "pasted plan"); + } + + private async Task OnFileSelected(InputFileChangeEventArgs e) + { + var file = e.File; + if (file.Size > 10 * 1024 * 1024) + { + errorMessage = $"File too large ({file.Size / (1024 * 1024)} MB). Maximum is 10 MB."; + return; + } + + try + { + using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); + using var reader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true); + var xml = await reader.ReadToEndAsync(); + await RunAnalysis(xml, file.Name); + } + catch (Exception ex) + { + errorMessage = $"Failed to read file: {ex.Message}"; + } + } + + private async Task RunAnalysis(string xml, string source) + { + errorMessage = null; + isAnalyzing = true; + activeStatement = 0; + StateHasChanged(); + await Task.Yield(); + + try + { + xml = xml.Replace("encoding=\"utf-16\"", "encoding=\"utf-8\""); + + parsedPlan = ShowPlanParser.Parse(xml); + + if (parsedPlan.ParseError != null) + { + errorMessage = $"Parse error: {parsedPlan.ParseError}"; + isAnalyzing = false; + return; + } + + PlanAnalyzer.Analyze(parsedPlan); + + foreach (var batch in parsedPlan.Batches) + { + foreach (var stmt in batch.Statements) + { + PlanLayoutEngine.Layout(stmt); + } + } + + result = ResultMapper.Map(parsedPlan, source); + textOutput = TextFormatter.Format(result); + sourceLabel = source; + } + catch (System.Xml.XmlException ex) + { + errorMessage = $"Invalid XML: {ex.Message}"; + } + catch (Exception ex) + { + errorMessage = $"Analysis failed: {ex.Message}"; + } + finally + { + isAnalyzing = false; + } + } + + private void Reset() + { + result = null; + parsedPlan = null; + textOutput = null; + errorMessage = null; + planXml = ""; + activeStatement = 0; + } + + private RenderFragment RenderPlanNodes(PlanNode node, bool isRoot) => builder => + { + var height = PlanLayoutEngine.GetNodeHeight(node); + var costClass = node.CostPercent >= 25 ? " expensive" : ""; + var warningClass = node.HasWarnings ? " has-warnings" : ""; + var parallelClass = node.Parallel ? " parallel" : ""; + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", $"plan-node{costClass}{warningClass}{parallelClass}"); + builder.AddAttribute(2, "style", + $"left: {node.X}px; top: {node.Y}px; width: {PlanLayoutEngine.NodeWidth}px; height: {height}px;"); + + var tooltip = BuildTooltip(node); + builder.AddAttribute(3, "title", tooltip); + + // Icon row + builder.OpenElement(4, "div"); + builder.AddAttribute(5, "class", "node-icon-row"); + + builder.OpenElement(6, "img"); + builder.AddAttribute(7, "src", $"icons/{node.IconName}.png"); + builder.AddAttribute(8, "class", "node-icon"); + builder.AddAttribute(9, "alt", node.PhysicalOp); + builder.CloseElement(); + + if (node.HasWarnings) + { + builder.OpenElement(10, "span"); + builder.AddAttribute(11, "class", "badge-warn"); + builder.AddContent(12, "\u26a0"); + builder.CloseElement(); + } + if (node.Parallel) + { + builder.OpenElement(13, "span"); + builder.AddAttribute(14, "class", "badge-parallel"); + builder.AddContent(15, "\u21c6"); + builder.CloseElement(); + } + + builder.CloseElement(); // node-icon-row + + // Operator name + var opLabel = node.PhysicalOp; + if (node.PhysicalOp == "Parallelism" && !string.IsNullOrEmpty(node.LogicalOp) + && node.LogicalOp != "Parallelism") + { + opLabel = $"Parallelism ({node.LogicalOp})"; + } + + builder.OpenElement(16, "div"); + builder.AddAttribute(17, "class", "node-op"); + builder.AddContent(18, opLabel); + builder.CloseElement(); + + // Cost % + var costColorClass = !node.HasActualStats && node.CostPercent >= 50 ? " cost-high" + : !node.HasActualStats && node.CostPercent >= 25 ? " cost-med" + : ""; + builder.OpenElement(19, "div"); + builder.AddAttribute(20, "class", $"node-cost{costColorClass}"); + builder.AddContent(21, $"Cost: {node.CostPercent}%"); + builder.CloseElement(); + + // Actual plan stats + if (node.HasActualStats) + { + var ownElapsedMs = GetOwnElapsedMs(node); + var ownCpuMs = GetOwnCpuMs(node); + var ownElapsedSec = ownElapsedMs / 1000.0; + var ownCpuSec = ownCpuMs / 1000.0; + + var elClass = ownElapsedSec >= 1.0 ? " time-high" : ownElapsedSec >= 0.1 ? " time-med" : ""; + builder.OpenElement(22, "div"); + builder.AddAttribute(23, "class", $"node-time{elClass}"); + builder.AddContent(24, $"{ownElapsedSec:F3}s"); + builder.CloseElement(); + + var cpuClass = ownCpuSec >= 1.0 ? " time-high" : ownCpuSec >= 0.1 ? " time-med" : ""; + builder.OpenElement(25, "div"); + builder.AddAttribute(26, "class", $"node-cpu{cpuClass}"); + builder.AddContent(27, $"CPU: {ownCpuSec:F3}s"); + builder.CloseElement(); + + var estRows = node.EstimateRows; + var ratio = estRows > 0 ? node.ActualRows / estRows : (node.ActualRows > 0 ? double.MaxValue : 1.0); + var rowClass = (ratio < 0.1 || ratio > 10.0) ? " rows-skewed" : ""; + var accuracy = estRows > 0 ? $" ({ratio * 100:F0}%)" : ""; + builder.OpenElement(28, "div"); + builder.AddAttribute(29, "class", $"node-rows{rowClass}"); + builder.AddContent(30, $"{node.ActualRows:N0} of {estRows:N0}{accuracy}"); + builder.CloseElement(); + } + else + { + builder.OpenElement(31, "div"); + builder.AddAttribute(32, "class", "node-rows"); + builder.AddContent(33, $"{node.EstimateRows:N0} est. rows"); + builder.CloseElement(); + } + + if (!string.IsNullOrEmpty(node.ObjectName)) + { + builder.OpenElement(34, "div"); + builder.AddAttribute(35, "class", "node-object"); + builder.AddContent(36, node.FullObjectName ?? node.ObjectName); + builder.CloseElement(); + } + + builder.CloseElement(); // plan-node + + foreach (var child in node.Children) + builder.AddContent(37, RenderPlanNodes(child, false)); + }; + + private RenderFragment RenderConnectors(PlanNode node) => builder => + { + foreach (var child in node.Children) + { + var rows = child.HasActualStats ? child.ActualRows : (long)child.EstimateRows; + var thickness = rows > 100000 ? 4.0 : rows > 10000 ? 2.5 : 1.5; + + var x1 = node.X + PlanLayoutEngine.NodeWidth; + var y1 = node.Y + PlanLayoutEngine.GetNodeHeight(node) / 2; + var x2 = child.X; + var y2 = child.Y + PlanLayoutEngine.GetNodeHeight(child) / 2; + + builder.OpenElement(0, "line"); + builder.AddAttribute(1, "x1", x1.ToString("F1")); + builder.AddAttribute(2, "y1", y1.ToString("F1")); + builder.AddAttribute(3, "x2", x2.ToString("F1")); + builder.AddAttribute(4, "y2", y2.ToString("F1")); + builder.AddAttribute(5, "class", "connector"); + builder.AddAttribute(6, "style", $"stroke-width: {thickness:F1}"); + builder.CloseElement(); + + builder.AddContent(7, RenderConnectors(child)); + } + }; + + private static string BuildTooltip(PlanNode node) + { + var parts = new List(); + parts.Add($"{node.PhysicalOp} (Node {node.NodeId})"); + + if (!string.IsNullOrEmpty(node.LogicalOp) && node.LogicalOp != node.PhysicalOp) + parts.Add($"Logical: {node.LogicalOp}"); + + if (node.HasActualStats) + { + parts.Add($"Actual rows: {node.ActualRows:N0}"); + parts.Add($"Estimated rows: {node.EstimateRows:N0}"); + if (node.ActualLogicalReads > 0) parts.Add($"Logical reads: {node.ActualLogicalReads:N0}"); + if (node.ActualPhysicalReads > 0) parts.Add($"Physical reads: {node.ActualPhysicalReads:N0}"); + } + else + { + parts.Add($"Estimated rows: {node.EstimateRows:N0}"); + } + + parts.Add($"Estimated cost: {node.EstimatedOperatorCost:N4}"); + parts.Add($"Subtree cost: {node.EstimatedTotalSubtreeCost:N4}"); + + if (!string.IsNullOrEmpty(node.ObjectName)) parts.Add($"Object: {node.FullObjectName ?? node.ObjectName}"); + if (!string.IsNullOrEmpty(node.IndexName)) parts.Add($"Index: {node.IndexName}"); + if (!string.IsNullOrEmpty(node.SeekPredicates)) parts.Add($"Seek: {node.SeekPredicates}"); + if (!string.IsNullOrEmpty(node.Predicate)) parts.Add($"Predicate: {node.Predicate}"); + if (!string.IsNullOrEmpty(node.OrderBy)) parts.Add($"Order by: {node.OrderBy}"); + if (!string.IsNullOrEmpty(node.OutputColumns)) parts.Add($"Output: {node.OutputColumns}"); + + foreach (var w in node.Warnings) + parts.Add($"[{w.Severity}] {w.WarningType}: {w.Message}"); + + return string.Join("\n", parts); + } + + private static long GetOwnElapsedMs(PlanNode node) + { + if (!node.HasActualStats) return 0; + var mode = node.ActualExecutionMode ?? node.ExecutionMode; + if (mode == "Batch") return node.ActualElapsedMs; + long childSum = 0; + foreach (var child in node.Children) + { + if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0) + childSum += child.Children.Where(c => c.HasActualStats).Select(c => c.ActualElapsedMs).DefaultIfEmpty(0).Max(); + else if (child.HasActualStats) childSum += child.ActualElapsedMs; + else childSum += GetOwnElapsedMs(child); + } + return Math.Max(0, node.ActualElapsedMs - childSum); + } + + private static long GetOwnCpuMs(PlanNode node) + { + if (!node.HasActualStats || node.ActualCPUMs == 0) return 0; + var mode = node.ActualExecutionMode ?? node.ExecutionMode; + if (mode == "Batch") return node.ActualCPUMs; + long childSum = 0; + foreach (var child in node.Children) + { + if (child.HasActualStats && child.ActualCPUMs > 0) childSum += child.ActualCPUMs; + else childSum += GetOwnCpuMs(child); + } + return Math.Max(0, node.ActualCPUMs - childSum); + } + + private static List GetAllWarnings(StatementResult stmt) + { + var warnings = new List(stmt.Warnings); + if (stmt.OperatorTree != null) + CollectNodeWarningsRecursive(stmt.OperatorTree, warnings); + return warnings; + } + + private static List CollectNodeWarnings(OperatorResult node) + { + var warnings = new List(); + CollectNodeWarningsRecursive(node, warnings); + return warnings; + } + + private static void CollectNodeWarningsRecursive(OperatorResult node, List warnings) + { + warnings.AddRange(node.Warnings); + foreach (var child in node.Children) + CollectNodeWarningsRecursive(child, warnings); + } + + private static string FormatKB(long kb) + { + if (kb < 1024) return $"{kb:N0} KB"; + if (kb < 1024 * 1024) return $"{kb / 1024.0:N1} MB"; + return $"{kb / (1024.0 * 1024.0):N2} GB"; + } +} diff --git a/src/PlanViewer.Web/PlanViewer.Web.csproj b/src/PlanViewer.Web/PlanViewer.Web.csproj new file mode 100644 index 0000000..07e5b54 --- /dev/null +++ b/src/PlanViewer.Web/PlanViewer.Web.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + PlanViewer.Web + 1.4.0 + Erik Darling + Darling Data LLC + SQL Performance Studio + Copyright (c) 2026 Erik Darling, Darling Data LLC + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PlanViewer.Web/Program.cs b/src/PlanViewer.Web/Program.cs new file mode 100644 index 0000000..8cf816a --- /dev/null +++ b/src/PlanViewer.Web/Program.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using PlanViewer.Web; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +await builder.Build().RunAsync(); diff --git a/src/PlanViewer.Web/Properties/launchSettings.json b/src/PlanViewer.Web/Properties/launchSettings.json new file mode 100644 index 0000000..0b4b00c --- /dev/null +++ b/src/PlanViewer.Web/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "PlanViewer.Web": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5280", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/PlanViewer.Web/_Imports.razor b/src/PlanViewer.Web/_Imports.razor new file mode 100644 index 0000000..f8fec2d --- /dev/null +++ b/src/PlanViewer.Web/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using PlanViewer.Web +@using PlanViewer.Web.Layout +@using PlanViewer.Core.Models +@using PlanViewer.Core.Services +@using PlanViewer.Core.Output diff --git a/src/PlanViewer.Web/wwwroot/.nojekyll b/src/PlanViewer.Web/wwwroot/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/src/PlanViewer.Web/wwwroot/CNAME b/src/PlanViewer.Web/wwwroot/CNAME new file mode 100644 index 0000000..e038185 --- /dev/null +++ b/src/PlanViewer.Web/wwwroot/CNAME @@ -0,0 +1 @@ +plans.erikdarling.com diff --git a/src/PlanViewer.Web/wwwroot/css/app.css b/src/PlanViewer.Web/wwwroot/css/app.css new file mode 100644 index 0000000..2a7c101 --- /dev/null +++ b/src/PlanViewer.Web/wwwroot/css/app.css @@ -0,0 +1,858 @@ +/* === Brand: Darling Data === */ +:root { + --brand-accent: #2eaef1; + --brand-icon: #858585; + --brand-bg: #333333; + + --bg: #ffffff; + --bg-surface: #f5f5f5; + --bg-input: #fafafa; + --text: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --accent: var(--brand-accent); + --accent-hover: #1a9de0; + --border: #e0e0e0; + --border-dark: #cccccc; + + --critical: #d32f2f; + --orange: #e67e22; + --orange-red: #e74c3c; + --warning-color: #f39c12; + --info: var(--brand-accent); + --missing: #8e44ad; + --sniffing: #e74c3c; + + /* Insight card backgrounds — muted versions of desktop colors */ + --card-runtime: #f0f4f8; + --card-indexes: #fef8f0; + --card-params: #f0f8f0; + --card-waits: #f0f4fa; + + --card-runtime-border: #c8d8e8; + --card-indexes-border: #e8d8c0; + --card-params-border: #c0d8c0; + --card-waits-border: #c0c8e0; + + /* Plan tree */ + --node-bg: #ffffff; + --node-border: #cccccc; + --node-expensive-bg: #fef0f0; + --node-expensive-border: var(--orange-red); + --connector: #999999; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + background: var(--bg); + color: var(--text); + font-family: 'Armata', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 14px; + line-height: 1.5; +} + +/* === Header === */ +header { + padding: 0.6rem 2rem; + background: var(--brand-bg); + border-bottom: 3px solid var(--accent); +} + +.header-content { + display: flex; + align-items: center; + gap: 1rem; + max-width: 1400px; + margin: 0 auto; +} + +.header-logo { + height: 28px; + width: auto; +} + +.header-divider { + width: 1px; + height: 20px; + background: #555; +} + +.header-title { + font-size: 1rem; + color: #e0e0e0; + letter-spacing: 0.5px; +} + +/* === Footer === */ +footer { + text-align: center; + padding: 1.5rem 2rem; + border-top: 1px solid var(--border); + margin-top: 2rem; + font-size: 0.8rem; + color: var(--text-muted); +} + +footer a { + color: var(--accent); + text-decoration: none; +} + +footer a:hover { + text-decoration: underline; +} + +.footer-sep { + color: var(--text-muted); + margin: 0 0.5rem; +} + +/* === Layout === */ +main { + max-width: 1400px; + margin: 0 auto; + padding: 1rem 2rem; +} + +/* === Landing Page === */ +.landing { + display: flex; + justify-content: center; + padding-top: 3rem; +} + +.landing-content { + max-width: 700px; + width: 100%; + text-align: center; +} + +.landing-content h2 { + font-size: 1.5rem; + color: var(--text); + margin-bottom: 0.25rem; + font-weight: 400; +} + +.privacy { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: 1.5rem; +} + +/* === Input Area === */ +.input-area { + text-align: left; +} + +.input-tabs { + display: flex; + border-bottom: 1px solid var(--border); + margin-bottom: 1rem; +} + +.tab { + padding: 0.5rem 1.5rem; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + border-bottom: 2px solid transparent; +} + +.tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.tab:hover { + color: var(--text); +} + +textarea { + width: 100%; + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.75rem; + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.85rem; + resize: vertical; + outline: none; +} + +textarea:focus { + border-color: var(--accent); +} + +textarea::placeholder { + color: var(--text-muted); +} + +.analyze-btn { + margin-top: 0.75rem; + padding: 0.6rem 2rem; + background: var(--accent); + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; +} + +.analyze-btn:hover:not(:disabled) { + background: var(--accent-hover); +} + +.analyze-btn:disabled { + opacity: 0.5; + cursor: default; +} + +.upload-area { + padding: 2rem; + border: 2px dashed var(--border); + border-radius: 4px; + text-align: center; +} + +.upload-hint { + margin-top: 0.5rem; + color: var(--text-muted); + font-size: 0.85rem; +} + +/* === Toolbar === */ +.toolbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 0; + margin-bottom: 0.75rem; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; +} + +.back-btn { + padding: 0.3rem 0.75rem; + background: var(--bg-surface); + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 0.8rem; +} + +.back-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.toolbar-source { + font-size: 0.85rem; + color: var(--text); + font-weight: 500; +} + +.toolbar-plan-type { + font-size: 0.75rem; + padding: 0.15rem 0.5rem; + border-radius: 3px; + font-weight: 500; +} + +.toolbar-plan-type.actual { + background: #e8f5e9; + color: #2e7d32; +} + +.toolbar-plan-type.estimated { + background: #fff3e0; + color: #e65100; +} + +.toolbar-version { + font-size: 0.8rem; + color: var(--text-muted); +} + +/* === Statement Tabs === */ +.statement-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--border); + margin-bottom: 0.75rem; + overflow-x: auto; +} + +.stmt-tab { + padding: 0.4rem 1rem; + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-secondary); + cursor: pointer; + font-family: inherit; + font-size: 0.8rem; + white-space: nowrap; + margin-bottom: -2px; +} + +.stmt-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.stmt-tab:hover { + color: var(--text); +} + +.stmt-tab-cost { + color: var(--text-muted); + margin-left: 0.25rem; + font-size: 0.75rem; +} + +.stmt-tab-warns { + display: inline-block; + background: var(--warning-color); + color: #fff; + font-size: 0.65rem; + padding: 0 0.35rem; + border-radius: 8px; + margin-left: 0.25rem; + font-weight: 600; +} + +/* === Insights Panel === */ +.insights-panel { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.insight-card { + border-radius: 6px; + border: 1px solid var(--border); + overflow: hidden; +} + +.insight-card h4 { + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + font-weight: 500; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.insight-body { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + max-height: 220px; + overflow-y: auto; +} + +.insight-count { + font-size: 0.7rem; + background: var(--bg-surface); + padding: 0.1rem 0.4rem; + border-radius: 8px; + color: var(--text-secondary); +} + +.insight-empty { + color: var(--text-muted); + font-size: 0.8rem; + font-style: italic; +} + +/* Card colors */ +.insight-card.runtime { + background: var(--card-runtime); + border-color: var(--card-runtime-border); +} +.insight-card.runtime h4 { color: #2c5282; } + +.insight-card.indexes { + background: var(--card-indexes); + border-color: var(--card-indexes-border); +} +.insight-card.indexes h4 { color: #9c4221; } + +.insight-card.params { + background: var(--card-params); + border-color: var(--card-params-border); +} +.insight-card.params h4 { color: #276749; } + +.insight-card.waits { + background: var(--card-waits); + border-color: var(--card-waits-border); +} +.insight-card.waits h4 { color: #2a4365; } + +/* Insight rows */ +.insight-row { + display: flex; + justify-content: space-between; + padding: 0.15rem 0; +} + +.insight-label { + color: var(--text-secondary); + font-size: 0.75rem; +} + +.insight-value { + font-weight: 500; + font-size: 0.8rem; +} + +.eff-good { color: #2e7d32; } +.eff-ok { color: #f9a825; } +.eff-warn { color: var(--orange); } +.eff-bad { color: var(--critical); } + +/* Missing indexes */ +.mi-item { + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--card-indexes-border); +} + +.mi-item:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.mi-table-name { + font-weight: 500; + font-size: 0.8rem; +} + +.mi-impact-row { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.mi-impact-val { + color: var(--orange); + font-weight: 500; +} + +.mi-create { + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.7rem; + background: rgba(255, 255, 255, 0.5); + padding: 0.3rem 0.5rem; + border-radius: 3px; + margin-top: 0.25rem; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} + +/* Parameters table */ +.params-table { + width: 100%; + border-collapse: collapse; + font-size: 0.75rem; +} + +.params-table th { + text-align: left; + font-weight: 500; + color: var(--text-secondary); + padding: 0.2rem 0.4rem; + border-bottom: 1px solid var(--card-params-border); + font-size: 0.7rem; +} + +.params-table td { + padding: 0.2rem 0.4rem; + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.7rem; +} + +.param-name-cell { + color: var(--accent); +} + +.param-type-cell { + color: var(--text-muted); +} + +.sniffing-row { + background: rgba(231, 76, 60, 0.08); +} + +.sniffing-val { + color: var(--sniffing); + font-weight: 600; +} + +/* Wait stats */ +.wait-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.15rem 0; + font-size: 0.75rem; +} + +.wait-type { + min-width: 120px; + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.7rem; +} + +.wait-bar-container { + flex: 1; + height: 8px; + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + overflow: hidden; +} + +.wait-bar { + height: 100%; + background: var(--accent); + border-radius: 4px; + opacity: 0.6; +} + +.wait-ms { + min-width: 60px; + text-align: right; + color: var(--text-secondary); + font-size: 0.7rem; +} + +/* === Warnings Strip === */ +.warnings-strip { + margin-bottom: 0.75rem; + border: 1px solid #f5c6cb; + border-radius: 6px; + background: #fff5f5; +} + +.warnings-strip h4 { + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + font-weight: 500; + color: var(--critical); + border-bottom: 1px solid #f5c6cb; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.warn-count-badge { + background: var(--critical); + color: #fff; + font-size: 0.65rem; + padding: 0.05rem 0.4rem; + border-radius: 8px; +} + +.warnings-list { + padding: 0.5rem 0.75rem; + max-height: 300px; + overflow-y: auto; +} + +.warning { + padding: 0.35rem 0.5rem; + margin-bottom: 0.25rem; + border-radius: 4px; + font-size: 0.8rem; + border-left: 3px solid var(--border); + background: rgba(255, 255, 255, 0.5); +} + +.warning.critical { border-left-color: var(--critical); } +.warning.warning { border-left-color: var(--warning-color); } +.warning.info { border-left-color: var(--accent); } + +.severity { + font-weight: 600; + margin-right: 0.5rem; + font-size: 0.7rem; + text-transform: uppercase; +} + +.critical .severity { color: var(--critical); } +.warning .severity { color: var(--warning-color); } +.info .severity { color: var(--accent); } + +.warning-type { + font-weight: 500; + margin-right: 0.5rem; + font-size: 0.8rem; +} + +.warning-op { + color: var(--text-muted); + margin-right: 0.5rem; + font-size: 0.75rem; +} + +.warning-msg { + color: var(--text-secondary); + display: block; + margin-top: 0.15rem; + font-size: 0.75rem; +} + +/* === Query Text === */ +.stmt-text-section { + margin-bottom: 0.75rem; + border: 1px solid var(--border); + border-radius: 6px; +} + +.stmt-text-section summary { + padding: 0.4rem 0.75rem; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.8rem; + background: var(--bg-surface); + border-radius: 6px; +} + +.stmt-text-section[open] summary { + border-bottom: 1px solid var(--border); + border-radius: 6px 6px 0 0; +} + +.statement-text { + padding: 0.75rem; + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.8rem; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; +} + +/* === Plan Tree === */ +.plan-tree-container { + overflow: auto; + margin-bottom: 0.75rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-surface); + max-height: 600px; +} + +.plan-tree { + position: relative; + min-width: 100%; +} + +.plan-connectors { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +.connector { + stroke: var(--connector); + stroke-width: 1.5; +} + +.plan-node { + position: absolute; + background: var(--node-bg); + border: 1px solid var(--node-border); + border-radius: 4px; + padding: 4px 8px; + font-size: 0.75rem; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + cursor: default; + transition: box-shadow 0.15s; +} + +.plan-node:hover { + box-shadow: 0 0 0 2px var(--accent); +} + +.plan-node.expensive { + background: var(--node-expensive-bg); + border-color: var(--node-expensive-border); + border-width: 2px; +} + +.plan-node.has-warnings { + border-color: var(--warning-color); +} + +/* Node icon row */ +.node-icon-row { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin-bottom: 2px; +} + +.node-icon { + width: 32px; + height: 32px; +} + +.badge-warn { + color: var(--orange); + font-size: 0.8rem; +} + +.badge-parallel { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + background: #ffc107; + color: #333; + font-size: 0.6rem; + font-weight: bold; +} + +.node-op { + font-weight: 600; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + max-width: 134px; + font-size: 0.7rem; +} + +.node-cost { + color: var(--text-secondary); + font-size: 0.65rem; +} + +.node-cost.cost-high { color: var(--orange-red); font-weight: 600; } +.node-cost.cost-med { color: var(--orange); font-weight: 500; } + +.node-time { font-size: 0.65rem; color: var(--text); } +.node-time.time-high { color: var(--orange-red); font-weight: 600; } +.node-time.time-med { color: var(--orange); } + +.node-cpu { font-size: 0.65rem; color: var(--text-secondary); } +.node-cpu.time-high { color: var(--orange-red); font-weight: 600; } +.node-cpu.time-med { color: var(--orange); } + +.node-rows { color: var(--text-secondary); font-size: 0.65rem; } +.node-rows.rows-skewed { color: var(--orange-red); } + +.node-object { + color: var(--accent); + font-size: 0.6rem; + overflow: hidden; + text-overflow: ellipsis; + max-width: 134px; +} + +/* === Text Output === */ +.text-output { + margin-bottom: 1rem; + border: 1px solid var(--border); + border-radius: 6px; +} + +.text-output summary { + padding: 0.5rem 0.75rem; + cursor: pointer; + color: var(--text-secondary); + font-size: 0.8rem; + background: var(--bg-surface); + border-radius: 6px; +} + +.text-output[open] summary { + border-bottom: 1px solid var(--border); + border-radius: 6px 6px 0 0; +} + +.text-output pre { + padding: 0.75rem; + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.8rem; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; +} + +/* === Error === */ +.error-message { + padding: 0.75rem 1rem; + margin-bottom: 1rem; + background: #fff5f5; + border: 1px solid var(--critical); + border-radius: 4px; + color: var(--critical); + font-size: 0.9rem; +} + +/* === Loading === */ +.loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + color: var(--text-muted); + font-size: 1.1rem; + font-family: 'Armata', sans-serif; +} + +/* === Blazor Error UI === */ +#blazor-error-ui { + display: none; + position: fixed; + bottom: 0; + width: 100%; + padding: 0.75rem; + background: #fff5f5; + color: var(--critical); + text-align: center; + font-size: 0.85rem; +} + +#blazor-error-ui .reload { + color: var(--accent); +} + +/* === Responsive === */ +@media (max-width: 900px) { + .insights-panel { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 600px) { + .insights-panel { + grid-template-columns: 1fr; + } +} diff --git a/src/PlanViewer.Web/wwwroot/darling-data-logo.png b/src/PlanViewer.Web/wwwroot/darling-data-logo.png new file mode 100644 index 0000000..94e7845 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/darling-data-logo.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/adaptive_join.png b/src/PlanViewer.Web/wwwroot/icons/adaptive_join.png new file mode 100644 index 0000000..a9de845 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/adaptive_join.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/aggregate.png b/src/PlanViewer.Web/wwwroot/icons/aggregate.png new file mode 100644 index 0000000..c9dfa1a Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/aggregate.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/apply.png b/src/PlanViewer.Web/wwwroot/icons/apply.png new file mode 100644 index 0000000..0e4972d Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/apply.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/arithmetic_expression.png b/src/PlanViewer.Web/wwwroot/icons/arithmetic_expression.png new file mode 100644 index 0000000..532e698 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/arithmetic_expression.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/assert.png b/src/PlanViewer.Web/wwwroot/icons/assert.png new file mode 100644 index 0000000..8c828bf Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/assert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/assign.png b/src/PlanViewer.Web/wwwroot/icons/assign.png new file mode 100644 index 0000000..6006369 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/assign.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/batch_hash_table_build.png b/src/PlanViewer.Web/wwwroot/icons/batch_hash_table_build.png new file mode 100644 index 0000000..f955ff4 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/batch_hash_table_build.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/bitmap.png b/src/PlanViewer.Web/wwwroot/icons/bitmap.png new file mode 100644 index 0000000..9010b0b Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/bitmap.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/bookmark_lookup.png b/src/PlanViewer.Web/wwwroot/icons/bookmark_lookup.png new file mode 100644 index 0000000..89d5d11 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/bookmark_lookup.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/broadcast.png b/src/PlanViewer.Web/wwwroot/icons/broadcast.png new file mode 100644 index 0000000..05735c5 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/broadcast.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_delete.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_delete.png new file mode 100644 index 0000000..6da544c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_insert.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_insert.png new file mode 100644 index 0000000..4706b31 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_merge.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_merge.png new file mode 100644 index 0000000..c819163 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_merge.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_scan.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_scan.png new file mode 100644 index 0000000..58faec2 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_seek.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_seek.png new file mode 100644 index 0000000..13094ba Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_seek.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_index_update.png b/src/PlanViewer.Web/wwwroot/icons/clustered_index_update.png new file mode 100644 index 0000000..956a813 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_index_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/clustered_update.png b/src/PlanViewer.Web/wwwroot/icons/clustered_update.png new file mode 100644 index 0000000..15ccb90 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/clustered_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/collapse.png b/src/PlanViewer.Web/wwwroot/icons/collapse.png new file mode 100644 index 0000000..7e24191 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/collapse.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/columnstore_index_delete.png b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_delete.png new file mode 100644 index 0000000..cac4731 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/columnstore_index_insert.png b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_insert.png new file mode 100644 index 0000000..110242b Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/columnstore_index_merge.png b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_merge.png new file mode 100644 index 0000000..cd5f764 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_merge.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/columnstore_index_scan.png b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_scan.png new file mode 100644 index 0000000..de030b6 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/columnstore_index_update.png b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_update.png new file mode 100644 index 0000000..f0b2ad1 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/columnstore_index_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/compute_scalar.png b/src/PlanViewer.Web/wwwroot/icons/compute_scalar.png new file mode 100644 index 0000000..7ce66ce Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/compute_scalar.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/compute_to_control_node.png b/src/PlanViewer.Web/wwwroot/icons/compute_to_control_node.png new file mode 100644 index 0000000..98c0c95 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/compute_to_control_node.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/concatenation.png b/src/PlanViewer.Web/wwwroot/icons/concatenation.png new file mode 100644 index 0000000..214d683 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/concatenation.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/const_table_get.png b/src/PlanViewer.Web/wwwroot/icons/const_table_get.png new file mode 100644 index 0000000..0f32261 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/const_table_get.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/constant_scan.png b/src/PlanViewer.Web/wwwroot/icons/constant_scan.png new file mode 100644 index 0000000..88c1354 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/constant_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/control_to_compute_nodes.png b/src/PlanViewer.Web/wwwroot/icons/control_to_compute_nodes.png new file mode 100644 index 0000000..47066d9 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/control_to_compute_nodes.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/convert.png b/src/PlanViewer.Web/wwwroot/icons/convert.png new file mode 100644 index 0000000..badf83c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/convert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/cursor_catch_all.png b/src/PlanViewer.Web/wwwroot/icons/cursor_catch_all.png new file mode 100644 index 0000000..7344f5d Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/cursor_catch_all.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/declare.png b/src/PlanViewer.Web/wwwroot/icons/declare.png new file mode 100644 index 0000000..7d7f97b Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/declare.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/delete.png b/src/PlanViewer.Web/wwwroot/icons/delete.png new file mode 100644 index 0000000..2d1e122 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/deleted_scan.png b/src/PlanViewer.Web/wwwroot/icons/deleted_scan.png new file mode 100644 index 0000000..84e6c0a Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/deleted_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/dynamic.png b/src/PlanViewer.Web/wwwroot/icons/dynamic.png new file mode 100644 index 0000000..01e9266 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/dynamic.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/external_broadcast.png b/src/PlanViewer.Web/wwwroot/icons/external_broadcast.png new file mode 100644 index 0000000..373f018 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/external_broadcast.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/external_export.png b/src/PlanViewer.Web/wwwroot/icons/external_export.png new file mode 100644 index 0000000..7533114 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/external_export.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/external_local_streaming.png b/src/PlanViewer.Web/wwwroot/icons/external_local_streaming.png new file mode 100644 index 0000000..48889ae Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/external_local_streaming.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/external_round_robin.png b/src/PlanViewer.Web/wwwroot/icons/external_round_robin.png new file mode 100644 index 0000000..b0202d4 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/external_round_robin.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/external_shuffle.png b/src/PlanViewer.Web/wwwroot/icons/external_shuffle.png new file mode 100644 index 0000000..6d7c24c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/external_shuffle.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/fetch_query.png b/src/PlanViewer.Web/wwwroot/icons/fetch_query.png new file mode 100644 index 0000000..a1fd0f1 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/fetch_query.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/filter.png b/src/PlanViewer.Web/wwwroot/icons/filter.png new file mode 100644 index 0000000..af726e9 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/filter.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/foreign_key_references_check.png b/src/PlanViewer.Web/wwwroot/icons/foreign_key_references_check.png new file mode 100644 index 0000000..f87b644 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/foreign_key_references_check.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/get.png b/src/PlanViewer.Web/wwwroot/icons/get.png new file mode 100644 index 0000000..79e832f Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/get.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/group_by_aggregate.png b/src/PlanViewer.Web/wwwroot/icons/group_by_aggregate.png new file mode 100644 index 0000000..cdb1bf9 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/group_by_aggregate.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/hash_match.png b/src/PlanViewer.Web/wwwroot/icons/hash_match.png new file mode 100644 index 0000000..fb78ddc Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/hash_match.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/hash_match_root.png b/src/PlanViewer.Web/wwwroot/icons/hash_match_root.png new file mode 100644 index 0000000..07f46f5 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/hash_match_root.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/hash_match_team.png b/src/PlanViewer.Web/wwwroot/icons/hash_match_team.png new file mode 100644 index 0000000..c107079 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/hash_match_team.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/if.png b/src/PlanViewer.Web/wwwroot/icons/if.png new file mode 100644 index 0000000..0aedbcb Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/if.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_delete.png b/src/PlanViewer.Web/wwwroot/icons/index_delete.png new file mode 100644 index 0000000..c367019 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_insert.png b/src/PlanViewer.Web/wwwroot/icons/index_insert.png new file mode 100644 index 0000000..723e71a Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_scan.png b/src/PlanViewer.Web/wwwroot/icons/index_scan.png new file mode 100644 index 0000000..e373d6c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_seek.png b/src/PlanViewer.Web/wwwroot/icons/index_seek.png new file mode 100644 index 0000000..093805c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_seek.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_spool.png b/src/PlanViewer.Web/wwwroot/icons/index_spool.png new file mode 100644 index 0000000..5d0fa8f Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_spool.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/index_update.png b/src/PlanViewer.Web/wwwroot/icons/index_update.png new file mode 100644 index 0000000..0b4570e Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/index_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/insert.png b/src/PlanViewer.Web/wwwroot/icons/insert.png new file mode 100644 index 0000000..87fca57 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/inserted_scan.png b/src/PlanViewer.Web/wwwroot/icons/inserted_scan.png new file mode 100644 index 0000000..4f2db17 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/inserted_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/intrinsic.png b/src/PlanViewer.Web/wwwroot/icons/intrinsic.png new file mode 100644 index 0000000..8c5b614 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/intrinsic.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/iterator_catch_all.png b/src/PlanViewer.Web/wwwroot/icons/iterator_catch_all.png new file mode 100644 index 0000000..5a2d728 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/iterator_catch_all.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/join.png b/src/PlanViewer.Web/wwwroot/icons/join.png new file mode 100644 index 0000000..aefe5b4 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/join.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/keyset.png b/src/PlanViewer.Web/wwwroot/icons/keyset.png new file mode 100644 index 0000000..f6371f4 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/keyset.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/language_construct_catch_all.png b/src/PlanViewer.Web/wwwroot/icons/language_construct_catch_all.png new file mode 100644 index 0000000..2d247ce Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/language_construct_catch_all.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/locate.png b/src/PlanViewer.Web/wwwroot/icons/locate.png new file mode 100644 index 0000000..5cba667 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/locate.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/log_row_scan.png b/src/PlanViewer.Web/wwwroot/icons/log_row_scan.png new file mode 100644 index 0000000..bfe7875 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/log_row_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/merge_interval.png b/src/PlanViewer.Web/wwwroot/icons/merge_interval.png new file mode 100644 index 0000000..98b2aef Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/merge_interval.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/merge_join.png b/src/PlanViewer.Web/wwwroot/icons/merge_join.png new file mode 100644 index 0000000..b9c1bce Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/merge_join.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/nested_loops.png b/src/PlanViewer.Web/wwwroot/icons/nested_loops.png new file mode 100644 index 0000000..89f6c76 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/nested_loops.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/parallelism.png b/src/PlanViewer.Web/wwwroot/icons/parallelism.png new file mode 100644 index 0000000..45d28d8 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/parallelism.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/parameter_table_scan.png b/src/PlanViewer.Web/wwwroot/icons/parameter_table_scan.png new file mode 100644 index 0000000..5dd33d6 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/parameter_table_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/population_query.png b/src/PlanViewer.Web/wwwroot/icons/population_query.png new file mode 100644 index 0000000..172c90e Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/population_query.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/predict.png b/src/PlanViewer.Web/wwwroot/icons/predict.png new file mode 100644 index 0000000..deb2b30 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/predict.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/print.png b/src/PlanViewer.Web/wwwroot/icons/print.png new file mode 100644 index 0000000..9ecab73 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/print.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/project.png b/src/PlanViewer.Web/wwwroot/icons/project.png new file mode 100644 index 0000000..be9d176 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/project.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/rank.png b/src/PlanViewer.Web/wwwroot/icons/rank.png new file mode 100644 index 0000000..0084590 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/rank.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/refresh_query.png b/src/PlanViewer.Web/wwwroot/icons/refresh_query.png new file mode 100644 index 0000000..eeb4e75 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/refresh_query.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_delete.png b/src/PlanViewer.Web/wwwroot/icons/remote_delete.png new file mode 100644 index 0000000..6c4d1b7 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_index_scan.png b/src/PlanViewer.Web/wwwroot/icons/remote_index_scan.png new file mode 100644 index 0000000..1f06dd5 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_index_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_index_seek.png b/src/PlanViewer.Web/wwwroot/icons/remote_index_seek.png new file mode 100644 index 0000000..94e66cb Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_index_seek.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_insert.png b/src/PlanViewer.Web/wwwroot/icons/remote_insert.png new file mode 100644 index 0000000..9a22f89 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_query.png b/src/PlanViewer.Web/wwwroot/icons/remote_query.png new file mode 100644 index 0000000..073dcef Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_query.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_scan.png b/src/PlanViewer.Web/wwwroot/icons/remote_scan.png new file mode 100644 index 0000000..24a43a9 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/remote_update.png b/src/PlanViewer.Web/wwwroot/icons/remote_update.png new file mode 100644 index 0000000..f027393 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/remote_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/result.png b/src/PlanViewer.Web/wwwroot/icons/result.png new file mode 100644 index 0000000..e3545a1 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/result.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/rid_lookup.png b/src/PlanViewer.Web/wwwroot/icons/rid_lookup.png new file mode 100644 index 0000000..83ad0c0 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/rid_lookup.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/row_count_spool.png b/src/PlanViewer.Web/wwwroot/icons/row_count_spool.png new file mode 100644 index 0000000..40ee6c5 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/row_count_spool.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/segment.png b/src/PlanViewer.Web/wwwroot/icons/segment.png new file mode 100644 index 0000000..64e1218 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/segment.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/sequence.png b/src/PlanViewer.Web/wwwroot/icons/sequence.png new file mode 100644 index 0000000..1a5aeb8 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/sequence.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/sequence_project.png b/src/PlanViewer.Web/wwwroot/icons/sequence_project.png new file mode 100644 index 0000000..29dc7b8 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/sequence_project.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/set_function.png b/src/PlanViewer.Web/wwwroot/icons/set_function.png new file mode 100644 index 0000000..7272565 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/set_function.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/shuffle.png b/src/PlanViewer.Web/wwwroot/icons/shuffle.png new file mode 100644 index 0000000..dafa538 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/shuffle.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/single_source_round_robin.png b/src/PlanViewer.Web/wwwroot/icons/single_source_round_robin.png new file mode 100644 index 0000000..e77bd9f Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/single_source_round_robin.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/single_source_shuffle.png b/src/PlanViewer.Web/wwwroot/icons/single_source_shuffle.png new file mode 100644 index 0000000..f8b27a7 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/single_source_shuffle.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/snapshot.png b/src/PlanViewer.Web/wwwroot/icons/snapshot.png new file mode 100644 index 0000000..110e191 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/snapshot.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/sort.png b/src/PlanViewer.Web/wwwroot/icons/sort.png new file mode 100644 index 0000000..5d36843 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/sort.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/split.png b/src/PlanViewer.Web/wwwroot/icons/split.png new file mode 100644 index 0000000..37c6369 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/split.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/spool.png b/src/PlanViewer.Web/wwwroot/icons/spool.png new file mode 100644 index 0000000..cc2a20d Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/spool.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/sql.png b/src/PlanViewer.Web/wwwroot/icons/sql.png new file mode 100644 index 0000000..112e580 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/sql.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/stream_aggregate.png b/src/PlanViewer.Web/wwwroot/icons/stream_aggregate.png new file mode 100644 index 0000000..04285c1 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/stream_aggregate.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/switch.png b/src/PlanViewer.Web/wwwroot/icons/switch.png new file mode 100644 index 0000000..20d342a Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/switch.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_delete.png b/src/PlanViewer.Web/wwwroot/icons/table_delete.png new file mode 100644 index 0000000..09ee862 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_delete.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_insert.png b/src/PlanViewer.Web/wwwroot/icons/table_insert.png new file mode 100644 index 0000000..3b515fd Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_insert.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_merge.png b/src/PlanViewer.Web/wwwroot/icons/table_merge.png new file mode 100644 index 0000000..f390461 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_merge.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_scan.png b/src/PlanViewer.Web/wwwroot/icons/table_scan.png new file mode 100644 index 0000000..ad807fe Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_scan.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_spool.png b/src/PlanViewer.Web/wwwroot/icons/table_spool.png new file mode 100644 index 0000000..3ba8e7c Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_spool.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_update.png b/src/PlanViewer.Web/wwwroot/icons/table_update.png new file mode 100644 index 0000000..171bc4f Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/table_valued_function.png b/src/PlanViewer.Web/wwwroot/icons/table_valued_function.png new file mode 100644 index 0000000..6969e59 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/table_valued_function.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/top.png b/src/PlanViewer.Web/wwwroot/icons/top.png new file mode 100644 index 0000000..e47f122 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/top.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/trim.png b/src/PlanViewer.Web/wwwroot/icons/trim.png new file mode 100644 index 0000000..b398045 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/trim.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/udx.png b/src/PlanViewer.Web/wwwroot/icons/udx.png new file mode 100644 index 0000000..59b331e Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/udx.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/union.png b/src/PlanViewer.Web/wwwroot/icons/union.png new file mode 100644 index 0000000..4bf96a9 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/union.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/union_all.png b/src/PlanViewer.Web/wwwroot/icons/union_all.png new file mode 100644 index 0000000..d199647 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/union_all.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/update.png b/src/PlanViewer.Web/wwwroot/icons/update.png new file mode 100644 index 0000000..ea13c72 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/update.png differ diff --git a/src/PlanViewer.Web/wwwroot/icons/window_aggregate.png b/src/PlanViewer.Web/wwwroot/icons/window_aggregate.png new file mode 100644 index 0000000..9f2f161 Binary files /dev/null and b/src/PlanViewer.Web/wwwroot/icons/window_aggregate.png differ diff --git a/src/PlanViewer.Web/wwwroot/index.html b/src/PlanViewer.Web/wwwroot/index.html new file mode 100644 index 0000000..a503897 --- /dev/null +++ b/src/PlanViewer.Web/wwwroot/index.html @@ -0,0 +1,25 @@ + + + + + + Performance Studio + + + + + + + +
+
Loading analysis engine...
+
+ +
+ An unhandled error has occurred. + Reload +
+ + + +