diff --git a/src/PlanViewer.Web/Pages/Index.razor b/src/PlanViewer.Web/Pages/Index.razor index 8e1cfd4..a57d061 100644 --- a/src/PlanViewer.Web/Pages/Index.razor +++ b/src/PlanViewer.Web/Pages/Index.razor @@ -63,7 +63,7 @@ else { var idx = si; var isActive = idx == activeStatement; - + + +
+ + @* General *@ +
+ General +
+
Physical Op@selectedNode.PhysicalOp
+
Logical Op@selectedNode.LogicalOp
+
Node ID@selectedNode.NodeId
+ @if (!string.IsNullOrEmpty(selectedNode.ExecutionMode)) + { +
Execution Mode@selectedNode.ExecutionMode
+ } + @if (!string.IsNullOrEmpty(selectedNode.ActualExecutionMode) && selectedNode.ActualExecutionMode != selectedNode.ExecutionMode) + { +
Actual Exec Mode@selectedNode.ActualExecutionMode
+ } +
Parallel@(selectedNode.Parallel ? "True" : "False")
+ @if (selectedNode.Partitioned) + { +
PartitionedYes
+ } + @if (selectedNode.EstimatedDOP > 0) + { +
Estimated DOP@selectedNode.EstimatedDOP
+ } + @if (!string.IsNullOrEmpty(selectedNode.FullObjectName)) + { +
Ordered@(selectedNode.Ordered ? "True" : "False")
+ @if (!string.IsNullOrEmpty(selectedNode.ScanDirection)) + { +
Scan Direction@selectedNode.ScanDirection
+ } +
Forced Index@(selectedNode.ForcedIndex ? "True" : "False")
+
ForceScan@(selectedNode.ForceScan ? "True" : "False")
+
ForceSeek@(selectedNode.ForceSeek ? "True" : "False")
+
NoExpandHint@(selectedNode.NoExpandHint ? "True" : "False")
+ @if (selectedNode.Lookup) + { +
LookupYes
+ } + @if (selectedNode.DynamicSeek) + { +
Dynamic SeekYes
+ } + } + @if (!string.IsNullOrEmpty(selectedNode.StorageType)) + { +
Storage@selectedNode.StorageType
+ } + @if (selectedNode.IsAdaptive) + { +
AdaptiveYes
+ } + @if (selectedNode.SpillOccurredDetail) + { +
Spill OccurredYes
+ } +
+
+ + @* Object *@ + @if (!string.IsNullOrEmpty(selectedNode.ObjectName)) + { +
+ Object +
+
Name@(selectedNode.FullObjectName ?? selectedNode.ObjectName)
+ @if (!string.IsNullOrEmpty(selectedNode.IndexName)) + { +
Index@selectedNode.IndexName
+ } + @if (!string.IsNullOrEmpty(selectedNode.IndexKind)) + { +
Index Kind@selectedNode.IndexKind
+ } + @if (selectedNode.FilteredIndex) + { +
FilteredYes
+ } + @if (!string.IsNullOrEmpty(selectedNode.ObjectAlias)) + { +
Alias@selectedNode.ObjectAlias
+ } + @if (!string.IsNullOrEmpty(selectedNode.DatabaseName)) + { +
Database@selectedNode.DatabaseName
+ } + @if (!string.IsNullOrEmpty(selectedNode.ServerName)) + { +
Server@selectedNode.ServerName
+ } + @if (selectedNode.TableReferenceId > 0) + { +
Table Ref ID@selectedNode.TableReferenceId
+ } +
+
+ } + + @* Costs *@ +
+ Costs +
+
Operator Cost@selectedNode.EstimatedOperatorCost.ToString("N4") (@(selectedNode.CostPercent)%)
+
Subtree Cost@selectedNode.EstimatedTotalSubtreeCost.ToString("N4")
+
I/O Cost@selectedNode.EstimateIO.ToString("N4")
+
CPU Cost@selectedNode.EstimateCPU.ToString("N4")
+
+
+ + @* Rows *@ +
+ Rows +
+
Est. Executions@((1 + selectedNode.EstimateRebinds).ToString("N0"))
+
Est. Rows Per Exec@selectedNode.EstimateRows.ToString("N1")
+
Est. Rows All Execs@((selectedNode.EstimateRows * Math.Max(1, 1 + selectedNode.EstimateRebinds)).ToString("N1"))
+ @if (selectedNode.EstimatedRowsRead > 0) + { +
Est. Rows Read@selectedNode.EstimatedRowsRead.ToString("N0")
+ } + @if (selectedNode.EstimatedRowSize > 0) + { +
Avg Row Size@selectedNode.EstimatedRowSize B
+ } + @if (selectedNode.TableCardinality > 0) + { +
Table Cardinality@selectedNode.TableCardinality.ToString("N0")
+ } + @if (selectedNode.EstimateRebinds > 0) + { +
Est. Rebinds@selectedNode.EstimateRebinds.ToString("N2")
+ } + @if (selectedNode.EstimateRewinds > 0) + { +
Est. Rewinds@selectedNode.EstimateRewinds.ToString("N2")
+ } + @if (selectedNode.EstimateRowsWithoutRowGoal > 0 && Math.Abs(selectedNode.EstimateRowsWithoutRowGoal - selectedNode.EstimateRows) > 0.01) + { +
Est. Rows (No Goal)@selectedNode.EstimateRowsWithoutRowGoal.ToString("N0")
+ } +
+
+ + @* Actual Statistics *@ + @if (selectedNode.HasActualStats) + { +
+ Actual Statistics +
+
Actual Rows@selectedNode.ActualRows.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats) + { +
Thread @t.ThreadId@t.ActualRows.ToString("N0")
+ } + } + @if (selectedNode.ActualRowsRead > 0) + { +
Actual Rows Read@selectedNode.ActualRowsRead.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualRowsRead > 0)) + { +
Thread @t.ThreadId@t.ActualRowsRead.ToString("N0")
+ } + } + } +
Actual Executions@selectedNode.ActualExecutions.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats) + { +
Thread @t.ThreadId@t.ActualExecutions.ToString("N0")
+ } + } + @if (selectedNode.ActualRebinds > 0) + { +
Actual Rebinds@selectedNode.ActualRebinds.ToString("N0")
+ } + @if (selectedNode.ActualRewinds > 0) + { +
Actual Rewinds@selectedNode.ActualRewinds.ToString("N0")
+ } + @if (selectedNode.PartitionsAccessed > 0) + { +
Partitions Accessed@selectedNode.PartitionsAccessed
+ @if (!string.IsNullOrEmpty(selectedNode.PartitionRanges)) + { +
Partition Ranges@selectedNode.PartitionRanges
+ } + } +
+
+ + @* Actual Timing *@ + @if (selectedNode.ActualElapsedMs > 0 || selectedNode.ActualCPUMs > 0 || selectedNode.UdfCpuTimeMs > 0 || selectedNode.UdfElapsedTimeMs > 0) + { +
+ Actual Timing +
+ @if (selectedNode.ActualElapsedMs > 0) + { +
Elapsed Time@FormatMs(selectedNode.ActualElapsedMs)
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualElapsedMs > 0)) + { +
Thread @t.ThreadId@FormatMs(t.ActualElapsedMs)
+ } + } + } + @if (selectedNode.ActualCPUMs > 0) + { +
CPU Time@FormatMs(selectedNode.ActualCPUMs)
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualCPUMs > 0)) + { +
Thread @t.ThreadId@FormatMs(t.ActualCPUMs)
+ } + } + } + @if (selectedNode.UdfElapsedTimeMs > 0) + { +
UDF Elapsed@FormatMs(selectedNode.UdfElapsedTimeMs)
+ } + @if (selectedNode.UdfCpuTimeMs > 0) + { +
UDF CPU@FormatMs(selectedNode.UdfCpuTimeMs)
+ } +
+
+ } + + @* Actual I/O *@ + @if (selectedNode.ActualLogicalReads > 0 || selectedNode.ActualPhysicalReads > 0 || selectedNode.ActualScans > 0 || selectedNode.ActualReadAheads > 0 || selectedNode.ActualSegmentReads > 0 || selectedNode.ActualSegmentSkips > 0) + { +
+ Actual I/O +
+
Logical Reads@selectedNode.ActualLogicalReads.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualLogicalReads > 0)) + { +
Thread @t.ThreadId@t.ActualLogicalReads.ToString("N0")
+ } + } + @if (selectedNode.ActualPhysicalReads > 0) + { +
Physical Reads@selectedNode.ActualPhysicalReads.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualPhysicalReads > 0)) + { +
Thread @t.ThreadId@t.ActualPhysicalReads.ToString("N0")
+ } + } + } + @if (selectedNode.ActualScans > 0) + { +
Scans@selectedNode.ActualScans.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualScans > 0)) + { +
Thread @t.ThreadId@t.ActualScans.ToString("N0")
+ } + } + } + @if (selectedNode.ActualReadAheads > 0) + { +
Read-Ahead Reads@selectedNode.ActualReadAheads.ToString("N0")
+ @if (selectedNode.PerThreadStats.Count > 1) + { + @foreach (var t in selectedNode.PerThreadStats.Where(t => t.ActualReadAheads > 0)) + { +
Thread @t.ThreadId@t.ActualReadAheads.ToString("N0")
+ } + } + } + @if (selectedNode.ActualSegmentReads > 0) + { +
Segment Reads@selectedNode.ActualSegmentReads.ToString("N0")
+ } + @if (selectedNode.ActualSegmentSkips > 0) + { +
Segment Skips@selectedNode.ActualSegmentSkips.ToString("N0")
+ } +
+
+ } + + @* Actual LOB I/O *@ + @if (selectedNode.ActualLobLogicalReads > 0 || selectedNode.ActualLobPhysicalReads > 0 || selectedNode.ActualLobReadAheads > 0) + { +
+ Actual LOB I/O +
+ @if (selectedNode.ActualLobLogicalReads > 0) + { +
LOB Logical Reads@selectedNode.ActualLobLogicalReads.ToString("N0")
+ } + @if (selectedNode.ActualLobPhysicalReads > 0) + { +
LOB Physical Reads@selectedNode.ActualLobPhysicalReads.ToString("N0")
+ } + @if (selectedNode.ActualLobReadAheads > 0) + { +
LOB Read-Aheads@selectedNode.ActualLobReadAheads.ToString("N0")
+ } +
+
+ } + } + + @* Predicates *@ + @if (HasPredicates(selectedNode)) + { +
+ Predicates +
+ @if (!string.IsNullOrEmpty(selectedNode.SeekPredicates)) + { +
Seek@selectedNode.SeekPredicates
+ } + @if (!string.IsNullOrEmpty(selectedNode.Predicate)) + { +
Predicate@selectedNode.Predicate
+ } + @if (!string.IsNullOrEmpty(selectedNode.HashKeysBuild)) + { +
Hash Keys (Build)@selectedNode.HashKeysBuild
+ } + @if (!string.IsNullOrEmpty(selectedNode.HashKeysProbe)) + { +
Hash Keys (Probe)@selectedNode.HashKeysProbe
+ } + @if (!string.IsNullOrEmpty(selectedNode.BuildResidual)) + { +
Build Residual@selectedNode.BuildResidual
+ } + @if (!string.IsNullOrEmpty(selectedNode.ProbeResidual)) + { +
Probe Residual@selectedNode.ProbeResidual
+ } + @if (!string.IsNullOrEmpty(selectedNode.MergeResidual)) + { +
Merge Residual@selectedNode.MergeResidual
+ } + @if (!string.IsNullOrEmpty(selectedNode.PassThru)) + { +
Pass Through@selectedNode.PassThru
+ } + @if (!string.IsNullOrEmpty(selectedNode.SetPredicate)) + { +
Set Predicate@selectedNode.SetPredicate
+ } + @if (selectedNode.GuessedSelectivity) + { +
Guessed SelectivityYes
+ } +
+
+ } + + @* Output *@ + @if (!string.IsNullOrEmpty(selectedNode.OutputColumns)) + { +
+ Output +
+
Columns@selectedNode.OutputColumns
+
+
+ } + + @* Operator Details *@ + @if (HasOperatorDetails(selectedNode)) + { +
+ Operator Details +
+ @if (!string.IsNullOrEmpty(selectedNode.OrderBy)) + { +
Order By@selectedNode.OrderBy
+ } + @if (!string.IsNullOrEmpty(selectedNode.GroupBy)) + { +
Group By@selectedNode.GroupBy
+ } + @if (!string.IsNullOrEmpty(selectedNode.TopExpression)) + { +
Top@selectedNode.TopExpression@(selectedNode.IsPercent ? " PERCENT" : "")@(selectedNode.WithTies ? " WITH TIES" : "")
+ } + @if (!string.IsNullOrEmpty(selectedNode.OffsetExpression)) + { +
Offset@selectedNode.OffsetExpression
+ } + @if (!string.IsNullOrEmpty(selectedNode.InnerSideJoinColumns)) + { +
Inner Join Cols@selectedNode.InnerSideJoinColumns
+ } + @if (!string.IsNullOrEmpty(selectedNode.OuterSideJoinColumns)) + { +
Outer Join Cols@selectedNode.OuterSideJoinColumns
+ } + @if (!string.IsNullOrEmpty(selectedNode.OuterReferences)) + { +
Outer References@selectedNode.OuterReferences
+ } + @if (!string.IsNullOrEmpty(selectedNode.DefinedValues)) + { +
Defined Values@selectedNode.DefinedValues
+ } + @if (selectedNode.ManyToMany) + { +
Many to ManyYes
+ } + @if (selectedNode.SortDistinct) + { +
Sort DistinctYes
+ } + @if (selectedNode.BitmapCreator) + { +
Bitmap CreatorYes
+ } + @if (selectedNode.NLOptimized) + { +
NL OptimizedYes
+ } + @if (selectedNode.WithOrderedPrefetch) + { +
Ordered PrefetchYes
+ } + @if (selectedNode.WithUnorderedPrefetch) + { +
Unordered PrefetchYes
+ } + @if (selectedNode.NonClusteredIndexCount > 0) + { +
NC Indexes@selectedNode.NonClusteredIndexCount maintained
+ @foreach (var ncIdx in selectedNode.NonClusteredIndexNames) + { +
@ncIdx
+ } + } + @if (!string.IsNullOrEmpty(selectedNode.HashKeys)) + { +
Hash Keys@selectedNode.HashKeys
+ } + @if (!string.IsNullOrEmpty(selectedNode.PartitionColumns)) + { +
Partition Cols@selectedNode.PartitionColumns
+ } + @if (!string.IsNullOrEmpty(selectedNode.SegmentColumn)) + { +
Segment Column@selectedNode.SegmentColumn
+ } + @if (selectedNode.StartupExpression) + { +
Startup ExpressionYes
+ } + @if (selectedNode.Remoting) + { +
RemotingYes
+ } + @if (selectedNode.LocalParallelism) + { +
Local ParallelismYes
+ } + @if (!string.IsNullOrEmpty(selectedNode.ConstantScanValues)) + { +
Constant Values@selectedNode.ConstantScanValues
+ } + @if (selectedNode.DMLRequestSort) + { +
DML Request SortYes
+ } + @if (!string.IsNullOrEmpty(selectedNode.ActionColumn)) + { +
Action Column@selectedNode.ActionColumn
+ } + @if (!string.IsNullOrEmpty(selectedNode.OriginalActionColumn)) + { +
Original Action Col@selectedNode.OriginalActionColumn
+ } + @if (!string.IsNullOrEmpty(selectedNode.TvfParameters)) + { +
TVF Parameters@selectedNode.TvfParameters
+ } + @if (!string.IsNullOrEmpty(selectedNode.UdxName)) + { +
UDX Name@selectedNode.UdxName
+ } + @if (!string.IsNullOrEmpty(selectedNode.UdxUsedColumns)) + { +
UDX Columns@selectedNode.UdxUsedColumns
+ } + @if (!string.IsNullOrEmpty(selectedNode.TieColumns)) + { +
Tie Columns@selectedNode.TieColumns
+ } + @if (selectedNode.InRow) + { +
In-RowYes
+ } + @if (selectedNode.ComputeSequence) + { +
Compute SequenceYes
+ } + @if (selectedNode.RollupHighestLevel > 0) + { +
Rollup Level@selectedNode.RollupHighestLevel
+ } + @if (selectedNode.RollupLevels.Count > 0) + { +
Rollup Levels@string.Join(", ", selectedNode.RollupLevels)
+ } + @if (selectedNode.SpoolStack) + { +
Spool StackYes
+ } + @if (selectedNode.PrimaryNodeId > 0) + { +
Primary Node ID@selectedNode.PrimaryNodeId
+ } + @if (selectedNode.IsStarJoin) + { +
Star JoinYes
+ } + @if (!string.IsNullOrEmpty(selectedNode.StarJoinOperationType)) + { +
Star Join Type@selectedNode.StarJoinOperationType
+ } + @if (!string.IsNullOrEmpty(selectedNode.ProbeColumn)) + { +
Probe Column@selectedNode.ProbeColumn
+ } + @if (!string.IsNullOrEmpty(selectedNode.PartitioningType)) + { +
Partitioning Type@selectedNode.PartitioningType
+ } + @if (!string.IsNullOrEmpty(selectedNode.PartitionId)) + { +
Partition ID@selectedNode.PartitionId
+ } + @if (selectedNode.ForceSeekColumnCount > 0) + { +
ForceSeek Cols@selectedNode.ForceSeekColumnCount
+ } + @if (selectedNode.RowCount) + { +
Row CountYes
+ } + @if (selectedNode.TopRows > 0) + { +
Top Rows@selectedNode.TopRows
+ } + @if (selectedNode.GroupExecuted) + { +
Group ExecutedYes
+ } + @if (selectedNode.RemoteDataAccess) + { +
Remote Data AccessYes
+ } + @if (selectedNode.OptimizedHalloweenProtectionUsed) + { +
Halloween ProtectionYes
+ } + @if (selectedNode.StatsCollectionId > 0) + { +
Stats Collection ID@selectedNode.StatsCollectionId
+ } +
+
+ } + + @* Adaptive Join *@ + @if (selectedNode.IsAdaptive) + { +
+ Adaptive Join +
+ @if (!string.IsNullOrEmpty(selectedNode.EstimatedJoinType)) + { +
Estimated Join@selectedNode.EstimatedJoinType
+ } + @if (!string.IsNullOrEmpty(selectedNode.ActualJoinType)) + { +
Actual Join@selectedNode.ActualJoinType
+ } + @if (selectedNode.AdaptiveThresholdRows > 0) + { +
Threshold Rows@selectedNode.AdaptiveThresholdRows.ToString("N0")
+ } +
+
+ } + + @* Scalar UDFs *@ + @if (selectedNode.ScalarUdfs.Count > 0) + { +
+ Scalar UDFs @selectedNode.ScalarUdfs.Count +
+ @foreach (var udf in selectedNode.ScalarUdfs) + { +
Function@udf.FunctionName
+ @if (udf.IsClrFunction) + { +
CLRYes
+ @if (!string.IsNullOrEmpty(udf.ClrAssembly)) + { +
Assembly@udf.ClrAssembly
+ } + @if (!string.IsNullOrEmpty(udf.ClrClass)) + { +
Class@udf.ClrClass
+ } + @if (!string.IsNullOrEmpty(udf.ClrMethod)) + { +
Method@udf.ClrMethod
+ } + } + } +
+
+ } + + @* Named Parameters *@ + @if (selectedNode.NamedParameters.Count > 0) + { +
+ Named Parameters +
+ @foreach (var np in selectedNode.NamedParameters) + { +
@np.Name@(np.ScalarString ?? "")
+ } +
+
+ } + + @* Operator Indexed Views *@ + @if (selectedNode.OperatorIndexedViews.Count > 0) + { +
+ Indexed Views +
+ @foreach (var iv in selectedNode.OperatorIndexedViews) + { +
@iv
+ } +
+
+ } + + @* Suggested Index *@ + @if (!string.IsNullOrEmpty(selectedNode.SuggestedIndex)) + { +
+ Suggested Index +
+
@selectedNode.SuggestedIndex
+
+
+ } + + @* Remote Operator *@ + @if (!string.IsNullOrEmpty(selectedNode.RemoteDestination) || !string.IsNullOrEmpty(selectedNode.RemoteSource) || !string.IsNullOrEmpty(selectedNode.RemoteObject) || !string.IsNullOrEmpty(selectedNode.RemoteQuery)) + { +
+ Remote Operator +
+ @if (!string.IsNullOrEmpty(selectedNode.RemoteDestination)) + { +
Destination@selectedNode.RemoteDestination
+ } + @if (!string.IsNullOrEmpty(selectedNode.RemoteSource)) + { +
Source@selectedNode.RemoteSource
+ } + @if (!string.IsNullOrEmpty(selectedNode.RemoteObject)) + { +
Object@selectedNode.RemoteObject
+ } + @if (!string.IsNullOrEmpty(selectedNode.RemoteQuery)) + { +
Query@selectedNode.RemoteQuery
+ } +
+
+ } + + @* Foreign Key References *@ + @if (selectedNode.ForeignKeyReferencesCount > 0) + { +
+ Foreign Key References +
+
FK References@selectedNode.ForeignKeyReferencesCount
+ @if (selectedNode.NoMatchingIndexCount > 0) + { +
No Matching Index@selectedNode.NoMatchingIndexCount
+ } + @if (selectedNode.PartialMatchingIndexCount > 0) + { +
Partial Match Index@selectedNode.PartialMatchingIndexCount
+ } +
+
+ } + + @* Memory *@ + @if (HasMemoryInfo(selectedNode)) + { +
+ Memory +
+ @if (selectedNode.MemoryGrantKB.HasValue && selectedNode.MemoryGrantKB > 0) + { +
Granted@FormatKB(selectedNode.MemoryGrantKB.Value)
+ } + @if (selectedNode.DesiredMemoryKB.HasValue && selectedNode.DesiredMemoryKB > 0) + { +
Desired@FormatKB(selectedNode.DesiredMemoryKB.Value)
+ } + @if (selectedNode.MaxUsedMemoryKB.HasValue && selectedNode.MaxUsedMemoryKB > 0) + { +
Max Used@FormatKB(selectedNode.MaxUsedMemoryKB.Value)
+ } + @if (selectedNode.InputMemoryGrantKB > 0) + { +
Input Grant@FormatKB(selectedNode.InputMemoryGrantKB)
+ } + @if (selectedNode.OutputMemoryGrantKB > 0) + { +
Output Grant@FormatKB(selectedNode.OutputMemoryGrantKB)
+ } + @if (selectedNode.UsedMemoryGrantKB > 0) + { +
Used Grant@FormatKB(selectedNode.UsedMemoryGrantKB)
+ } + @if (selectedNode.MemoryFractionInput > 0) + { +
Fraction Input@selectedNode.MemoryFractionInput.ToString("F4")
+ } + @if (selectedNode.MemoryFractionOutput > 0) + { +
Fraction Output@selectedNode.MemoryFractionOutput.ToString("F4")
+ } +
+
+ } + + @* Warnings *@ + @if (selectedNode.HasWarnings) + { +
+ Warnings @selectedNode.Warnings.Count +
+ @foreach (var w in selectedNode.Warnings) + { +
+ @w.WarningType + @w.Message +
+ } +
+
+ } + + @* Statement Info (root node only) *@ + @if (IsRootNode && ActiveStmtPlan != null) + { +
+ Statement Info +
+ @if (!string.IsNullOrEmpty(ActiveStmtPlan.StatementOptmLevel)) + { +
Optimization@ActiveStmtPlan.StatementOptmLevel
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.StatementOptmEarlyAbortReason)) + { +
Early Abort@ActiveStmtPlan.StatementOptmEarlyAbortReason
+ } + @if (ActiveStmtPlan.CardinalityEstimationModelVersion > 0) + { +
CE Model@ActiveStmtPlan.CardinalityEstimationModelVersion
+ } + @if (ActiveStmtPlan.DegreeOfParallelism > 0) + { +
DOP@ActiveStmtPlan.DegreeOfParallelism
+ } + @if (ActiveStmtPlan.CompileTimeMs > 0) + { +
Compile Time@FormatMs(ActiveStmtPlan.CompileTimeMs)
+ } + @if (ActiveStmtPlan.CompileCPUMs > 0) + { +
Compile CPU@FormatMs(ActiveStmtPlan.CompileCPUMs)
+ } + @if (ActiveStmtPlan.CompileMemoryKB > 0) + { +
Compile Memory@FormatKB(ActiveStmtPlan.CompileMemoryKB)
+ } + @if (ActiveStmtPlan.CachedPlanSizeKB > 0) + { +
Cached Plan Size@FormatKB(ActiveStmtPlan.CachedPlanSizeKB)
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.QueryHash)) + { +
Query Hash@ActiveStmtPlan.QueryHash
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.QueryPlanHash)) + { +
Plan Hash@ActiveStmtPlan.QueryPlanHash
+ } + @if (ActiveStmtPlan.BatchModeOnRowStoreUsed) + { +
Batch on RowStoreYes
+ } + @if (ActiveStmtPlan.SecurityPolicyApplied) + { +
Security PolicyYes
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.NonParallelPlanReason)) + { +
Non-Parallel Reason@ActiveStmtPlan.NonParallelPlanReason
+ } + @if (ActiveStmtPlan.EffectiveDOP > 0) + { +
Effective DOP@ActiveStmtPlan.EffectiveDOP
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.DOPFeedbackAdjusted)) + { +
DOP Feedback@ActiveStmtPlan.DOPFeedbackAdjusted
+ } + @if (ActiveStmtPlan.MaxQueryMemoryKB > 0) + { +
Max Query Memory@FormatKB(ActiveStmtPlan.MaxQueryMemoryKB)
+ } + @if (ActiveStmtPlan.QueryPlanMemoryGrantKB > 0) + { +
Plan Memory Grant@FormatKB(ActiveStmtPlan.QueryPlanMemoryGrantKB)
+ } + @if (ActiveStmtPlan.RetrievedFromCache) + { +
From CacheYes
+ } + @if (ActiveStmtPlan.StatementParameterizationType > 0) + { +
Parameterization@(ActiveStmtPlan.StatementParameterizationType == 1 ? "Forced" : ActiveStmtPlan.StatementParameterizationType == 2 ? "Simple" : ActiveStmtPlan.StatementParameterizationType.ToString())
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.StatementSqlHandle)) + { +
SQL Handle@ActiveStmtPlan.StatementSqlHandle
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.PlanGuideName)) + { +
Plan Guide@ActiveStmtPlan.PlanGuideName
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.PlanGuideDB)) + { +
Plan Guide DB@ActiveStmtPlan.PlanGuideDB
+ } + @if (ActiveStmtPlan.UsePlan) + { +
USE PLANYes
+ } + @if (ActiveStmtPlan.QueryStoreStatementHintId > 0) + { +
QS Hint ID@ActiveStmtPlan.QueryStoreStatementHintId
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.QueryStoreStatementHintText)) + { +
QS Hint@ActiveStmtPlan.QueryStoreStatementHintText
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.QueryStoreStatementHintSource)) + { +
QS Hint Source@ActiveStmtPlan.QueryStoreStatementHintSource
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.ParameterizedText)) + { +
Parameterized Text@ActiveStmtPlan.ParameterizedText
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.StmtUseDatabaseName)) + { +
USE Database@ActiveStmtPlan.StmtUseDatabaseName
+ } + @if (ActiveStmtPlan.DatabaseContextSettingsId > 0) + { +
DB Settings ID@ActiveStmtPlan.DatabaseContextSettingsId
+ } + @if (ActiveStmtPlan.ParentObjectId > 0) + { +
Parent Object ID@ActiveStmtPlan.ParentObjectId
+ } +
+
+ } + + @* Cursor Info (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && !string.IsNullOrEmpty(ActiveStmtPlan.CursorName)) + { +
+ Cursor Info +
+
Name@ActiveStmtPlan.CursorName
+ @if (!string.IsNullOrEmpty(ActiveStmtPlan.CursorActualType)) + { +
Actual Type@ActiveStmtPlan.CursorActualType
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.CursorRequestedType)) + { +
Requested Type@ActiveStmtPlan.CursorRequestedType
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.CursorConcurrency)) + { +
Concurrency@ActiveStmtPlan.CursorConcurrency
+ } + @if (ActiveStmtPlan.CursorForwardOnly) + { +
Forward OnlyYes
+ } +
+
+ } + + @* Memory Grant Info (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.MemoryGrant != null) + { +
+ Memory Grant Info +
+ @if (ActiveStmtPlan.MemoryGrant.GrantedMemoryKB > 0) + { +
Granted@FormatKB(ActiveStmtPlan.MemoryGrant.GrantedMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.MaxUsedMemoryKB > 0) + { +
Max Used@FormatKB(ActiveStmtPlan.MemoryGrant.MaxUsedMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.RequestedMemoryKB > 0) + { +
Requested@FormatKB(ActiveStmtPlan.MemoryGrant.RequestedMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.DesiredMemoryKB > 0) + { +
Desired@FormatKB(ActiveStmtPlan.MemoryGrant.DesiredMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.RequiredMemoryKB > 0) + { +
Required@FormatKB(ActiveStmtPlan.MemoryGrant.RequiredMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.SerialRequiredMemoryKB > 0) + { +
Serial Required@FormatKB(ActiveStmtPlan.MemoryGrant.SerialRequiredMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.SerialDesiredMemoryKB > 0) + { +
Serial Desired@FormatKB(ActiveStmtPlan.MemoryGrant.SerialDesiredMemoryKB)
+ } + @if (ActiveStmtPlan.MemoryGrant.GrantWaitTimeMs > 0) + { +
Grant Wait Time@FormatMs(ActiveStmtPlan.MemoryGrant.GrantWaitTimeMs)
+ } + @if (ActiveStmtPlan.MemoryGrant.LastRequestedMemoryKB > 0) + { +
Last Requested@FormatKB(ActiveStmtPlan.MemoryGrant.LastRequestedMemoryKB)
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.MemoryGrant.IsMemoryGrantFeedbackAdjusted)) + { +
Feedback Adjusted@ActiveStmtPlan.MemoryGrant.IsMemoryGrantFeedbackAdjusted
+ } +
+
+ } + + @* Feature Flags (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && (ActiveStmtPlan.ContainsInterleavedExecutionCandidates || ActiveStmtPlan.ContainsInlineScalarTsqlUdfs || ActiveStmtPlan.ContainsLedgerTables || ActiveStmtPlan.ExclusiveProfileTimeActive || ActiveStmtPlan.QueryCompilationReplay > 0 || ActiveStmtPlan.QueryVariantID > 0)) + { +
+ Feature Flags +
+ @if (ActiveStmtPlan.ContainsInterleavedExecutionCandidates) + { +
Interleaved ExecYes
+ } + @if (ActiveStmtPlan.ContainsInlineScalarTsqlUdfs) + { +
Inline Scalar UDFsYes
+ } + @if (ActiveStmtPlan.ContainsLedgerTables) + { +
Ledger TablesYes
+ } + @if (ActiveStmtPlan.ExclusiveProfileTimeActive) + { +
Exclusive ProfileYes
+ } + @if (ActiveStmtPlan.QueryCompilationReplay > 0) + { +
Compilation Replay@ActiveStmtPlan.QueryCompilationReplay
+ } + @if (ActiveStmtPlan.QueryVariantID > 0) + { +
Query Variant ID@ActiveStmtPlan.QueryVariantID
+ } +
+
+ } + + @* Set Options (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.SetOptions != null) + { +
+ Set Options +
+
ANSI_NULLS@(ActiveStmtPlan.SetOptions.AnsiNulls ? "ON" : "OFF")
+
ANSI_PADDING@(ActiveStmtPlan.SetOptions.AnsiPadding ? "ON" : "OFF")
+
ANSI_WARNINGS@(ActiveStmtPlan.SetOptions.AnsiWarnings ? "ON" : "OFF")
+
ARITHABORT@(ActiveStmtPlan.SetOptions.ArithAbort ? "ON" : "OFF")
+
CONCAT_NULL@(ActiveStmtPlan.SetOptions.ConcatNullYieldsNull ? "ON" : "OFF")
+
NUMERIC_ROUNDABORT@(ActiveStmtPlan.SetOptions.NumericRoundAbort ? "ON" : "OFF")
+
QUOTED_IDENTIFIER@(ActiveStmtPlan.SetOptions.QuotedIdentifier ? "ON" : "OFF")
+
+
+ } + + @* Hardware Properties (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.HardwareProperties != null) + { +
+ Hardware +
+ @if (ActiveStmtPlan.HardwareProperties.EstimatedAvailableMemoryGrant > 0) + { +
Available Memory@FormatKB(ActiveStmtPlan.HardwareProperties.EstimatedAvailableMemoryGrant)
+ } + @if (ActiveStmtPlan.HardwareProperties.EstimatedPagesCached > 0) + { +
Pages Cached@ActiveStmtPlan.HardwareProperties.EstimatedPagesCached.ToString("N0")
+ } + @if (ActiveStmtPlan.HardwareProperties.EstimatedAvailableDOP > 0) + { +
Available DOP@ActiveStmtPlan.HardwareProperties.EstimatedAvailableDOP
+ } + @if (ActiveStmtPlan.HardwareProperties.MaxCompileMemory > 0) + { +
Max Compile Memory@FormatKB(ActiveStmtPlan.HardwareProperties.MaxCompileMemory)
+ } +
+
+ } + + @* Handles (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && (!string.IsNullOrEmpty(ActiveStmtPlan.ParameterizedPlanHandle) || !string.IsNullOrEmpty(ActiveStmtPlan.BatchSqlHandle) || !string.IsNullOrEmpty(ActiveStmtPlan.DispatcherPlanHandle))) + { +
+ Handles +
+ @if (!string.IsNullOrEmpty(ActiveStmtPlan.ParameterizedPlanHandle)) + { +
Parameterized Plan@ActiveStmtPlan.ParameterizedPlanHandle
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.BatchSqlHandle)) + { +
Batch SQL Handle@ActiveStmtPlan.BatchSqlHandle
+ } + @if (!string.IsNullOrEmpty(ActiveStmtPlan.DispatcherPlanHandle)) + { +
Dispatcher Plan@ActiveStmtPlan.DispatcherPlanHandle
+ } +
+
+ } + + @* Trace Flags (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && ActiveStmtPlan.TraceFlags.Count > 0) + { +
+ Trace Flags @ActiveStmtPlan.TraceFlags.Count +
+ @foreach (var tf in ActiveStmtPlan.TraceFlags) + { +
TF @tf.Value@tf.Scope@(tf.IsCompileTime ? " (compile)" : "")
+ } +
+
+ } + + @* PSP Dispatcher (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.Dispatcher != null && (ActiveStmtPlan.Dispatcher.ParameterSensitivePredicates.Count > 0 || ActiveStmtPlan.Dispatcher.OptionalParameterPredicates.Count > 0)) + { +
+ PSP Dispatcher +
+ @foreach (var psp in ActiveStmtPlan.Dispatcher.ParameterSensitivePredicates) + { + @if (!string.IsNullOrEmpty(psp.PredicateText)) + { +
Predicate@psp.PredicateText
+ } +
Range[@psp.LowBoundary.ToString("N0") — @psp.HighBoundary.ToString("N0")]
+ @foreach (var stat in psp.Statistics) + { +
@(!string.IsNullOrEmpty(stat.TableName) ? $"{stat.TableName}.{stat.StatisticsName}" : stat.StatisticsName)Modified: @stat.ModificationCount.ToString("N0"), Sampled: @stat.SamplingPercent.ToString("F1")%
+ } + } + @foreach (var opt in ActiveStmtPlan.Dispatcher.OptionalParameterPredicates) + { + @if (!string.IsNullOrEmpty(opt.PredicateText)) + { +
Optional@opt.PredicateText
+ } + } +
+
+ } + + @* Cardinality Feedback (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && ActiveStmtPlan.CardinalityFeedback.Count > 0) + { +
+ Cardinality Feedback +
+ @foreach (var cf in ActiveStmtPlan.CardinalityFeedback) + { +
Node @cf.Key@cf.Value.ToString("N0") rows
+ } +
+
+ } + + @* Optimization Replay (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && !string.IsNullOrEmpty(ActiveStmtPlan.OptimizationReplayScript)) + { +
+ Optimization Replay +
+
@ActiveStmtPlan.OptimizationReplayScript
+
+
+ } + + @* Template Plan Guide (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && !string.IsNullOrEmpty(ActiveStmtPlan.TemplatePlanGuideName)) + { +
+ Template Plan Guide +
+
Name@ActiveStmtPlan.TemplatePlanGuideName
+ @if (!string.IsNullOrEmpty(ActiveStmtPlan.TemplatePlanGuideDB)) + { +
Database@ActiveStmtPlan.TemplatePlanGuideDB
+ } +
+
+ } + + @* Plan Version (root only) *@ + @if (IsRootNode && parsedPlan != null && (!string.IsNullOrEmpty(parsedPlan.BuildVersion) || !string.IsNullOrEmpty(parsedPlan.Build))) + { +
+ Plan Version +
+ @if (!string.IsNullOrEmpty(parsedPlan.BuildVersion)) + { +
Build Version@parsedPlan.BuildVersion
+ } + @if (!string.IsNullOrEmpty(parsedPlan.Build)) + { +
Build@parsedPlan.Build
+ } + @if (parsedPlan.ClusteredMode) + { +
Clustered ModeYes
+ } +
+
+ } + + @* Statistics Used (root only) *@ + @if (IsRootNode && ActiveStmtPlan != null && ActiveStmtPlan.StatsUsage.Count > 0) + { +
+ Statistics Used @ActiveStmtPlan.StatsUsage.Count +
+ @foreach (var stat in ActiveStmtPlan.StatsUsage) + { +
@(!string.IsNullOrEmpty(stat.TableName) ? $"{stat.TableName}.{stat.StatisticsName}" : stat.StatisticsName)Mod: @stat.ModificationCount.ToString("N0"), @stat.SamplingPercent.ToString("F1")%@(!string.IsNullOrEmpty(stat.LastUpdate) ? $", {stat.LastUpdate}" : "")
+ } +
+
+ } + + @* Thread Stats (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.ThreadStats != null) + { +
+ Thread Stats +
+
Branches@ActiveStmtPlan.ThreadStats.Branches
+
Used Threads@ActiveStmtPlan.ThreadStats.UsedThreads
+ @if (ActiveStmtPlan.ThreadStats.Reservations.Sum(r => r.ReservedThreads) > 0) + { +
Reserved Threads@ActiveStmtPlan.ThreadStats.Reservations.Sum(r => r.ReservedThreads)
+ @foreach (var res in ActiveStmtPlan.ThreadStats.Reservations) + { +
Node @res.NodeId@res.ReservedThreads reserved
+ } + } +
+
+ } + + @* Query Time Stats (root only) *@ + @if (IsRootNode && ActiveStmtPlan?.QueryTimeStats != null) + { +
+ Query Time Stats +
+
CPU Time@FormatMs(ActiveStmtPlan.QueryTimeStats.CpuTimeMs)
+
Elapsed Time@FormatMs(ActiveStmtPlan.QueryTimeStats.ElapsedTimeMs)
+ @if (ActiveStmtPlan.QueryUdfCpuTimeMs > 0) + { +
UDF CPU Time@FormatMs(ActiveStmtPlan.QueryUdfCpuTimeMs)
+ } + @if (ActiveStmtPlan.QueryUdfElapsedTimeMs > 0) + { +
UDF Elapsed Time@FormatMs(ActiveStmtPlan.QueryUdfElapsedTimeMs)
+ } +
+
+ } + + @* Indexed Views (root only, statement-level) *@ + @if (IsRootNode && ActiveStmtPlan != null && ActiveStmtPlan.IndexedViews.Count > 0) + { +
+ Indexed Views +
+ @foreach (var iv in ActiveStmtPlan.IndexedViews) + { +
View@iv
+ } +
+
+ } + +
+ + } } @code { @@ -295,6 +1570,7 @@ else private string? textOutput; private string? sourceLabel; private int activeStatement = 0; + private PlanNode? selectedNode; private StatementResult? ActiveStmt => result?.Statements.ElementAtOrDefault(activeStatement); private PlanStatement? ActiveStmtPlan => parsedPlan?.Batches.SelectMany(b => b.Statements).ElementAtOrDefault(activeStatement); @@ -389,6 +1665,7 @@ else errorMessage = null; planXml = ""; activeStatement = 0; + selectedNode = null; } private RenderFragment RenderPlanNodes(PlanNode node, bool isRoot) => builder => @@ -399,12 +1676,14 @@ else var parallelClass = node.Parallel ? " parallel" : ""; builder.OpenElement(0, "div"); - builder.AddAttribute(1, "class", $"plan-node{costClass}{warningClass}{parallelClass}"); + var selectedClass = node == selectedNode ? " selected" : ""; + builder.AddAttribute(1, "class", $"plan-node{costClass}{warningClass}{parallelClass}{selectedClass}"); 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); + builder.AddAttribute(50, "onclick", EventCallback.Factory.Create(this, () => SelectNode(node))); // Icon row builder.OpenElement(4, "div"); @@ -625,4 +1904,90 @@ else if (kb < 1024 * 1024) return $"{kb / 1024.0:N1} MB"; return $"{kb / (1024.0 * 1024.0):N2} GB"; } + + private static string FormatMs(long ms) + { + if (ms < 1000) return $"{ms:N0} ms"; + return $"{ms / 1000.0:F3} s"; + } + + private void SelectNode(PlanNode node) + { + selectedNode = selectedNode == node ? null : node; + } + + private void CloseProperties() + { + selectedNode = null; + } + + private void SelectStatement(int idx) + { + activeStatement = idx; + selectedNode = null; + } + + private bool IsRootNode => selectedNode != null && ActiveStmtPlan?.RootNode == selectedNode; + + private static string GetOperatorLabel(PlanNode node) + { + if (node.PhysicalOp == "Parallelism" && !string.IsNullOrEmpty(node.LogicalOp) && node.LogicalOp != "Parallelism") + return $"Parallelism ({node.LogicalOp})"; + return node.PhysicalOp; + } + + private static bool HasPredicates(PlanNode node) => + !string.IsNullOrEmpty(node.SeekPredicates) || + !string.IsNullOrEmpty(node.Predicate) || + !string.IsNullOrEmpty(node.HashKeysProbe) || + !string.IsNullOrEmpty(node.HashKeysBuild) || + !string.IsNullOrEmpty(node.BuildResidual) || + !string.IsNullOrEmpty(node.ProbeResidual) || + !string.IsNullOrEmpty(node.MergeResidual) || + !string.IsNullOrEmpty(node.PassThru) || + !string.IsNullOrEmpty(node.SetPredicate); + + private static bool HasOperatorDetails(PlanNode node) => + !string.IsNullOrEmpty(node.OrderBy) || + !string.IsNullOrEmpty(node.GroupBy) || + !string.IsNullOrEmpty(node.TopExpression) || + !string.IsNullOrEmpty(node.InnerSideJoinColumns) || + !string.IsNullOrEmpty(node.OuterSideJoinColumns) || + !string.IsNullOrEmpty(node.OuterReferences) || + !string.IsNullOrEmpty(node.DefinedValues) || + !string.IsNullOrEmpty(node.HashKeys) || + !string.IsNullOrEmpty(node.PartitionColumns) || + !string.IsNullOrEmpty(node.SegmentColumn) || + !string.IsNullOrEmpty(node.ConstantScanValues) || + !string.IsNullOrEmpty(node.ActionColumn) || + !string.IsNullOrEmpty(node.OriginalActionColumn) || + !string.IsNullOrEmpty(node.OffsetExpression) || + !string.IsNullOrEmpty(node.TvfParameters) || + !string.IsNullOrEmpty(node.UdxName) || + !string.IsNullOrEmpty(node.UdxUsedColumns) || + !string.IsNullOrEmpty(node.TieColumns) || + !string.IsNullOrEmpty(node.PartitioningType) || + !string.IsNullOrEmpty(node.PartitionId) || + !string.IsNullOrEmpty(node.StarJoinOperationType) || + !string.IsNullOrEmpty(node.ProbeColumn) || + node.ManyToMany || node.SortDistinct || node.BitmapCreator || + node.NLOptimized || node.WithOrderedPrefetch || node.WithUnorderedPrefetch || + node.Remoting || node.LocalParallelism || node.StartupExpression || + node.DMLRequestSort || node.SpoolStack || node.WithTies || + node.IsStarJoin || node.InRow || node.ComputeSequence || + node.RowCount || node.GroupExecuted || node.RemoteDataAccess || + node.OptimizedHalloweenProtectionUsed || + node.NonClusteredIndexCount > 0 || node.TopRows > 0 || + node.RollupHighestLevel > 0 || node.ForceSeekColumnCount > 0 || + node.StatsCollectionId > 0; + + private static bool HasMemoryInfo(PlanNode node) => + (node.MemoryGrantKB.HasValue && node.MemoryGrantKB > 0) || + (node.DesiredMemoryKB.HasValue && node.DesiredMemoryKB > 0) || + (node.MaxUsedMemoryKB.HasValue && node.MaxUsedMemoryKB > 0) || + node.InputMemoryGrantKB > 0 || + node.OutputMemoryGrantKB > 0 || + node.UsedMemoryGrantKB > 0 || + node.MemoryFractionInput > 0 || + node.MemoryFractionOutput > 0; } diff --git a/src/PlanViewer.Web/wwwroot/css/app.css b/src/PlanViewer.Web/wwwroot/css/app.css index 2a7c101..7a6f18e 100644 --- a/src/PlanViewer.Web/wwwroot/css/app.css +++ b/src/PlanViewer.Web/wwwroot/css/app.css @@ -689,7 +689,7 @@ textarea::placeholder { justify-content: center; align-items: center; text-align: center; - cursor: default; + cursor: pointer; transition: box-shadow 0.15s; } @@ -856,3 +856,223 @@ textarea::placeholder { grid-template-columns: 1fr; } } + +/* === Selected Node === */ +.plan-node.selected { + box-shadow: 0 0 0 2px var(--accent); + border-color: var(--accent); + background: #f0f8ff; +} + +/* === Properties Panel === */ +.properties-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 380px; + z-index: 100; + background: var(--bg); + border-left: 2px solid var(--accent); + display: flex; + flex-direction: column; + box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08); + animation: slide-in 0.15s ease-out; +} + +@keyframes slide-in { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} + +/* Push main content when panel is open */ +main:has(.properties-panel) { + padding-right: 390px; + max-width: none; +} + +.prop-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.prop-header-info { + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 0; +} + +.prop-header-icon { + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.prop-header-op { + font-weight: 600; + font-size: 0.9rem; + color: var(--text); + line-height: 1.3; +} + +.prop-header-sub { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.prop-close { + background: none; + border: none; + font-size: 1.4rem; + color: var(--text-muted); + cursor: pointer; + padding: 0 0.25rem; + line-height: 1; + flex-shrink: 0; +} + +.prop-close:hover { + color: var(--text); +} + +.prop-body { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; +} + +/* Sections */ +.prop-section { + border-bottom: 1px solid var(--border); +} + +.prop-section summary { + padding: 0.4rem 1rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--accent); + cursor: pointer; + user-select: none; + background: var(--bg); +} + +.prop-section summary:hover { + background: var(--bg-surface); +} + +.prop-section[open] summary { + border-bottom: 1px solid var(--border); +} + +/* Property grid */ +.prop-grid { + padding: 0.35rem 1rem; +} + +.prop-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 0.2rem 0; + gap: 0.75rem; + font-size: 0.78rem; +} + +.prop-row.full { + flex-direction: column; + gap: 0.15rem; +} + +.prop-row.indent { + padding-left: 1rem; +} + +.prop-label { + color: var(--text-secondary); + font-size: 0.75rem; + flex-shrink: 0; + white-space: nowrap; +} + +.prop-value { + text-align: right; + color: var(--text); + font-weight: 500; + word-break: break-word; + min-width: 0; +} + +.prop-row.full .prop-value { + text-align: left; +} + +.prop-value.code { + font-family: 'Cascadia Code', 'Consolas', monospace; + font-size: 0.72rem; + font-weight: 400; + text-align: left; + background: var(--bg-surface); + padding: 0.15rem 0.4rem; + border-radius: 3px; + white-space: pre-wrap; + word-break: break-all; +} + +.prop-value.flag { + color: var(--accent); + font-weight: 600; +} + +.prop-value.flag.warn { + color: var(--orange-red); +} + +/* Warnings in panel */ +.prop-warn-count { + font-size: 0.65rem; + background: var(--critical); + color: #fff; + padding: 0.05rem 0.4rem; + border-radius: 8px; + font-weight: 600; +} + +.prop-warning { + padding: 0.3rem 0; + border-bottom: 1px solid var(--border); + font-size: 0.78rem; +} + +.prop-warning:last-child { + border-bottom: none; +} + +.prop-warning-type { + font-weight: 600; + color: var(--orange); + display: block; + font-size: 0.75rem; +} + +.prop-warning-msg { + color: var(--text-secondary); + display: block; + font-size: 0.72rem; + margin-top: 0.1rem; +} + +/* Properties panel responsive */ +@media (max-width: 700px) { + .properties-panel { + width: 100%; + } + main:has(.properties-panel) { + padding-right: 0; + } +}