YamlImportPage.razor 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. @page "/yaml/import"
  2. @using RackPeek.Domain.Api
  3. @using RackPeek.Domain.Persistence
  4. <PageTitle>Yaml Import</PageTitle>
  5. @inject UpsertInventoryUseCase ImportUseCase
  6. <div class="border border-zinc-800 rounded p-4 bg-zinc-900 mt-6">
  7. <div class="text-zinc-100 mb-3">
  8. Import YAML
  9. </div>
  10. <!-- Mode selector -->
  11. <div class="mb-4 flex gap-6 text-xs">
  12. <label class="flex items-start gap-2 cursor-pointer">
  13. <input type="radio"
  14. name="mergeMode"
  15. checked="@(_mode == MergeMode.Merge)"
  16. @onchange="() => OnModeChanged(MergeMode.Merge)"/>
  17. <div>
  18. <div class="text-emerald-400">Merge</div>
  19. <div class="text-zinc-500">
  20. Update matching resources. Properties not specified remain unchanged.
  21. </div>
  22. </div>
  23. </label>
  24. <label class="flex items-start gap-2 cursor-pointer">
  25. <input type="radio"
  26. name="mergeMode"
  27. checked="@(_mode == MergeMode.Replace)"
  28. @onchange="() => OnModeChanged(MergeMode.Replace)"/>
  29. <div>
  30. <div class="text-red-400">Replace</div>
  31. <div class="text-zinc-500">
  32. Completely replace matching resources.
  33. </div>
  34. </div>
  35. </label>
  36. </div>
  37. <!-- YAML Input -->
  38. <textarea class="w-full input font-mono text-xs mb-3"
  39. style="min-height: 18rem"
  40. placeholder="Paste YAML here..."
  41. @bind="_inputYaml"
  42. @bind:event="oninput"
  43. @bind:after="ComputeDiff">
  44. </textarea>
  45. @if (!string.IsNullOrEmpty(_validationError))
  46. {
  47. <div class="text-red-400 text-xs mb-3">
  48. @_validationError
  49. </div>
  50. }
  51. <!-- Apply -->
  52. <div class="flex gap-3 text-xs mb-4">
  53. <button class="text-amber-400 hover:text-amber-300 disabled:opacity-40"
  54. disabled="@(!_isValid)"
  55. @onclick="Apply">
  56. Apply
  57. </button>
  58. </div>
  59. <!-- Summary + Diff -->
  60. @if (_isValid)
  61. {
  62. <div class="mt-4 text-xs">
  63. <div class="mb-2 text-zinc-400">
  64. Import Summary
  65. </div>
  66. @if (!_added.Any() && !_updated.Any() && !_replaced.Any())
  67. {
  68. <div class="text-zinc-500 italic mt-2">
  69. No changes detected.
  70. </div>
  71. }
  72. @if (_added.Any())
  73. {
  74. <div class="text-emerald-400 mb-2">
  75. + @_added.Count added
  76. </div>
  77. @foreach (var name in _added)
  78. {
  79. <div class="ml-4 text-zinc-300 mb-4">
  80. <div class="font-bold">+ @name</div>
  81. <pre class="bg-zinc-800 p-2 rounded text-xs overflow-x-auto">
  82. @_newYaml[name]
  83. </pre>
  84. </div>
  85. }
  86. }
  87. @if (_updated.Any())
  88. {
  89. <div class="text-amber-400 mt-4 mb-2">
  90. ~ @_updated.Count updated
  91. </div>
  92. @foreach (var name in _updated)
  93. {
  94. <div class="ml-4 mb-6">
  95. <div class="font-bold text-zinc-300 mb-2">~ @name</div>
  96. <div class="grid grid-cols-2 gap-4">
  97. <div>
  98. <div class="text-zinc-500 mb-1">Current</div>
  99. <pre class="bg-zinc-800 p-2 rounded text-xs overflow-x-auto">
  100. @_oldYaml[name]
  101. </pre>
  102. </div>
  103. <div>
  104. <div class="text-emerald-500 mb-1">Incoming (Merged)</div>
  105. <pre class="bg-zinc-800 p-2 rounded text-xs overflow-x-auto">
  106. @_newYaml[name]
  107. </pre>
  108. </div>
  109. </div>
  110. </div>
  111. }
  112. }
  113. @if (_replaced.Any())
  114. {
  115. <div class="text-red-400 mt-4 mb-2">
  116. ! @_replaced.Count replaced
  117. </div>
  118. @foreach (var name in _replaced)
  119. {
  120. <div class="ml-4 mb-6">
  121. <div class="font-bold text-zinc-300 mb-2">! @name</div>
  122. <div class="grid grid-cols-2 gap-4">
  123. <div>
  124. <div class="text-zinc-500 mb-1">Current</div>
  125. <pre class="bg-zinc-800 p-2 rounded text-xs overflow-x-auto">
  126. @_oldYaml[name]
  127. </pre>
  128. </div>
  129. <div>
  130. <div class="text-red-400 mb-1">Incoming (Replacement)</div>
  131. <pre class="bg-zinc-800 p-2 rounded text-xs overflow-x-auto">
  132. @_newYaml[name]
  133. </pre>
  134. </div>
  135. </div>
  136. </div>
  137. }
  138. }
  139. </div>
  140. }
  141. </div>
  142. @code {
  143. private List<string> _added = new();
  144. private List<string> _updated = new();
  145. private List<string> _replaced = new();
  146. private Dictionary<string, string> _oldYaml = new(StringComparer.OrdinalIgnoreCase);
  147. private Dictionary<string, string> _newYaml = new(StringComparer.OrdinalIgnoreCase);
  148. private string _inputYaml = "";
  149. private string? _validationError;
  150. private bool _isValid;
  151. private MergeMode _mode = MergeMode.Merge;
  152. async Task OnModeChanged(MergeMode mode)
  153. {
  154. _mode = mode;
  155. await ComputeDiff();
  156. }
  157. async Task ComputeDiff()
  158. {
  159. _validationError = null;
  160. _isValid = false;
  161. _added.Clear();
  162. _updated.Clear();
  163. _replaced.Clear();
  164. _oldYaml.Clear();
  165. _newYaml.Clear();
  166. if (string.IsNullOrWhiteSpace(_inputYaml))
  167. return;
  168. try
  169. {
  170. var result = await ImportUseCase.ExecuteAsync(new ImportYamlRequest
  171. {
  172. Yaml = _inputYaml,
  173. Mode = _mode,
  174. DryRun = true
  175. });
  176. _added = result.Added;
  177. _updated = result.Updated;
  178. _replaced = result.Replaced;
  179. _oldYaml = result.OldYaml;
  180. _newYaml = result.NewYaml;
  181. _isValid = true;
  182. }
  183. catch (Exception ex)
  184. {
  185. _validationError = $"YAML invalid: {ex.Message}";
  186. }
  187. }
  188. async Task Apply()
  189. {
  190. if (!_isValid)
  191. return;
  192. try
  193. {
  194. await ImportUseCase.ExecuteAsync(new ImportYamlRequest
  195. {
  196. Yaml = _inputYaml,
  197. Mode = _mode,
  198. DryRun = false
  199. });
  200. _inputYaml = "";
  201. _isValid = false;
  202. _added.Clear();
  203. _updated.Clear();
  204. _replaced.Clear();
  205. }
  206. catch (Exception ex)
  207. {
  208. _validationError = $"Apply failed: {ex.Message}";
  209. }
  210. }
  211. }