@hot-updater/console 1.0.0-rc.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
  3. package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
  7. package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DCM0wr6O.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
  12. package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
  13. package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
  14. package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
  15. package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
  16. package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
  17. package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
  18. package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
  19. package/.output/public/assets/empty-CuT2UhFp.js +1 -0
  20. package/.output/public/assets/field-BBDpjIqu.js +1 -0
  21. package/.output/public/assets/index-6o51GkFe.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-CmraDzQh.js +1 -0
  24. package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
  25. package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
  26. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  27. package/.output/public/assets/insights-vNRCrfnS.js +42 -0
  28. package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
  29. package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
  31. package/.output/public/assets/installations-BazSJXE0.js +2 -0
  32. package/.output/public/assets/installations-ClHTqdAO.js +1 -0
  33. package/.output/public/assets/installations-DpKsBuHi.js +1 -0
  34. package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
  35. package/.output/public/assets/list-CgrYiadM.js +1 -0
  36. package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
  37. package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
  38. package/.output/public/assets/remote-config-Br56BODb.js +6 -0
  39. package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
  40. package/.output/public/assets/routes-B88Erkpk.js +2 -0
  41. package/.output/public/assets/routes-CfzsAleC.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-BZZBXMkr.js +1 -0
  44. package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
  45. package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
  46. package/.output/public/assets/styles-DTGEAJFv.css +2 -0
  47. package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
  55. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  56. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  57. package/.output/server/_libs/lucide-react.mjs +221 -9
  58. package/.output/server/_libs/next-themes.mjs +1 -1
  59. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  60. package/.output/server/_libs/recharts+[...].mjs +483 -901
  61. package/.output/server/_libs/sonner.mjs +1 -1
  62. package/.output/server/_libs/tanstack__history.mjs +68 -41
  63. package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
  64. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  65. package/.output/server/_libs/verkit.mjs +102 -1
  66. package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
  69. package/.output/server/_ssr/{HashValueDisplay-C5tS4oGo.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
  70. package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-WiIAKl4L.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
  74. package/.output/server/_ssr/{api-keys-aXriWrv7.mjs → api-keys-B4sYm8dM.mjs} +48 -9
  75. package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
  76. package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
  77. package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
  78. package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-B7w3AdRr.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
  82. package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
  83. package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
  84. package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
  85. package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
  86. package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
  87. package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
  89. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
  90. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
  91. package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
  92. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
  93. package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
  94. package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
  96. package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
  97. package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
  98. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
  99. package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
  100. package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-Coj6SwVP.mjs → input-group-CuYJHosb.mjs} +2 -2
  103. package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
  104. package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
  105. package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-l2-oBYth.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
  118. package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
  119. package/.output/server/_ssr/{insights_.distribution-INw6QxbU.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
  120. package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
  121. package/.output/server/_ssr/{installations-DH-tQuRo.mjs → installations-BSCmFq8Q.mjs} +279 -367
  122. package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
  125. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  126. package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
  127. package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
  128. package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
  129. package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
  130. package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
  131. package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
  132. package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-DfFyAp5p.mjs → router-D8NuoJ_I.mjs} +117 -136
  135. package/.output/server/_ssr/{routes-BtsfVx-p.mjs → routes-DgvnFqDy.mjs} +195 -429
  136. package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
  138. package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
  139. package/.output/server/_ssr/{select-CyvLaMza.mjs → select-t3iXRwc9.mjs} +3 -3
  140. package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-C-dLcQuy.mjs → sidebar-DJcErBMo.mjs} +107 -47
  142. package/.output/server/_ssr/ssr.mjs +1 -1
  143. package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
  144. package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
  145. package/.output/server/_ssr/{table-CkVhsNpe.mjs → table-37Dd23TN.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
  150. package/.output/server/index.mjs +303 -205
  151. package/LICENSE +13 -14
  152. package/README.md +80 -71
  153. package/lib/index.d.mts +23 -11
  154. package/lib/vite-internal.mjs +13 -3
  155. package/lib/vite.d.mts +2 -0
  156. package/package.json +14 -14
  157. package/src/components/AppSidebar.tsx +74 -75
  158. package/src/components/ConsoleAccessPage.tsx +5 -1
  159. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  160. package/src/components/NotFoundPage.tsx +7 -7
  161. package/src/components/features/FeatureSlots.tsx +103 -0
  162. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  163. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  164. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  165. package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
  166. package/src/components/features/insights/AppUsage.tsx +41 -14
  167. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  168. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  169. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  170. package/src/components/features/insights/EventDetails.tsx +390 -26
  171. package/src/components/features/insights/EventHistoryCard.tsx +111 -25
  172. package/src/components/features/insights/EventHistoryList.tsx +1 -1
  173. package/src/components/features/insights/FailureReports.tsx +407 -0
  174. package/src/components/features/insights/InsightsControls.tsx +240 -142
  175. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  176. package/src/components/features/insights/InsightsOverview.tsx +601 -403
  177. package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
  178. package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
  179. package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
  180. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  181. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  182. package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
  183. package/src/components/features/insights/RollbackButton.tsx +118 -0
  184. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  185. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  186. package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
  187. package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
  188. package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
  189. package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
  190. package/src/components/features/remote-config/ParametersCard.tsx +254 -0
  191. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  192. package/src/components/features/remote-config/PublishDialog.tsx +325 -0
  193. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  194. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  195. package/src/components/features/remote-config/ValueInput.tsx +151 -0
  196. package/src/components/features/remote-config/VersionsCard.tsx +384 -0
  197. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  198. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  199. package/src/components/ui/sidebar.tsx +22 -29
  200. package/src/index.ts +26 -19
  201. package/src/lib/api-keys-api.ts +0 -28
  202. package/src/lib/api-keys-rpc.ts +27 -38
  203. package/src/lib/api-rpc.ts +125 -288
  204. package/src/lib/api.ts +15 -134
  205. package/src/lib/bundle-activity.ts +24 -4
  206. package/src/lib/console-access.ts +20 -0
  207. package/src/lib/console-features-api.ts +68 -0
  208. package/src/lib/console-features-rpc.ts +13 -0
  209. package/src/lib/console-features.ts +98 -0
  210. package/src/lib/insights-api.ts +56 -2
  211. package/src/lib/insights-errors.ts +64 -0
  212. package/src/lib/insights-failures-rpc.ts +36 -0
  213. package/src/lib/insights-failures.ts +130 -0
  214. package/src/lib/insights-recovery-rpc.ts +13 -17
  215. package/src/lib/insights-recovery.ts +12 -36
  216. package/src/lib/insights-retention.ts +21 -0
  217. package/src/lib/insights-rpc.ts +30 -12
  218. package/src/lib/insights-search.ts +50 -4
  219. package/src/lib/insights-usage-rpc.ts +15 -9
  220. package/src/lib/insights-usage.ts +41 -3
  221. package/src/lib/insights-view.ts +34 -5
  222. package/src/lib/release-adoption-api.ts +214 -0
  223. package/src/lib/release-adoption-rpc.ts +49 -0
  224. package/src/lib/release-adoption.ts +164 -0
  225. package/src/lib/remote-config-api.ts +119 -0
  226. package/src/lib/remote-config-draft.ts +666 -0
  227. package/src/lib/remote-config-rpc.ts +196 -0
  228. package/src/lib/scroll-restoration.ts +21 -0
  229. package/src/lib/server/auth.server.ts +13 -5
  230. package/src/lib/server/bundleActivity.ts +27 -28
  231. package/src/lib/server/config.server.ts +21 -76
  232. package/src/lib/server/console-feature-apis.ts +70 -0
  233. package/src/lib/server/console-runtime.server.ts +82 -38
  234. package/src/lib/server/downloadBundle.ts +14 -15
  235. package/src/lib/server/failureReports.ts +50 -0
  236. package/src/lib/server/getBundleChildren.ts +31 -122
  237. package/src/lib/server/insightsUsage.ts +52 -113
  238. package/src/lib/server/listReleases.ts +109 -44
  239. package/src/lib/server/releaseAdoption.ts +151 -0
  240. package/src/lib/server/releaseReachability.ts +8 -8
  241. package/src/lib/server/remoteConfig.ts +75 -0
  242. package/src/lib/server/runtime.server.ts +120 -31
  243. package/src/lib/server/updateFailures.ts +66 -0
  244. package/src/lib/use-dialog-target.ts +27 -0
  245. package/src/routeTree.gen.ts +44 -44
  246. package/src/router.tsx +3 -2
  247. package/src/routes/-installations-search.ts +32 -8
  248. package/src/routes/-releases-search.ts +76 -29
  249. package/src/routes/__root.tsx +7 -1
  250. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  251. package/src/routes/api-keys.tsx +5 -2
  252. package/src/routes/index.tsx +210 -152
  253. package/src/routes/insights.tsx +91 -23
  254. package/src/routes/insights_.distribution.tsx +50 -9
  255. package/src/routes/installations.tsx +97 -40
  256. package/src/routes/remote-config.tsx +42 -0
  257. package/src/styles.css +9 -0
  258. package/src/vite-internal.ts +15 -3
  259. package/src/vite.ts +2 -0
  260. package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
  261. package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
  262. package/.output/public/assets/api-CdUyUqzR.js +0 -42
  263. package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
  264. package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
  265. package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
  266. package/.output/public/assets/card-CYHs5FZv.js +0 -1
  267. package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
  268. package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
  269. package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
  270. package/.output/public/assets/dist-BR4GzeEu.js +0 -1
  271. package/.output/public/assets/field-B3CYDpjg.js +0 -1
  272. package/.output/public/assets/index-C39o7rJH.js +0 -10
  273. package/.output/public/assets/info-C3y9HNoU.js +0 -1
  274. package/.output/public/assets/input-group-nLZylhWF.js +0 -1
  275. package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
  276. package/.output/public/assets/insights-DS3iO7Up.js +0 -1
  277. package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
  278. package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
  279. package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
  280. package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
  281. package/.output/public/assets/installations-CkFW4fV9.js +0 -2
  282. package/.output/public/assets/installations-DxTaIcHD.js +0 -1
  283. package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
  284. package/.output/public/assets/list-Dqt53fWZ.js +0 -1
  285. package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
  286. package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
  287. package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
  288. package/.output/public/assets/routes-Cgtvspjg.js +0 -2
  289. package/.output/public/assets/routes-O2r2UUN8.js +0 -1
  290. package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
  291. package/.output/public/assets/signing-BgspC-xP.js +0 -1
  292. package/.output/public/assets/styles-DnrWuBwd.css +0 -2
  293. package/.output/public/assets/table-BuWQgxJJ.js +0 -1
  294. package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
  295. package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
  296. package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
  297. package/.output/server/__23tanstack-start-server-fn-resolver-CmYkol9D.mjs +0 -150
  298. package/.output/server/_ssr/InsightsPageHeader-Dma3dVj1.mjs +0 -64
  299. package/.output/server/_ssr/api-C3bGmgY0.mjs +0 -332
  300. package/.output/server/_ssr/api-keys-api-BRF6rbRC.mjs +0 -60
  301. package/.output/server/_ssr/api-keys-rpc-BMa1ZM6N.mjs +0 -136
  302. package/.output/server/_ssr/api-rpc-BYQ2e0z6.mjs +0 -1399
  303. package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
  304. package/.output/server/_ssr/bundle-signing-rpc-DbQ4pFAX.mjs +0 -16
  305. package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
  306. package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
  307. package/.output/server/_ssr/card-B7KSO_25.mjs +0 -108
  308. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  309. package/.output/server/_ssr/config.server-C-d1QaBU.mjs +0 -368
  310. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  311. package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
  312. package/.output/server/_ssr/field-BDwuU8RE.mjs +0 -74
  313. package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
  314. package/.output/server/_ssr/insights-BDJjaDLp.mjs +0 -938
  315. package/.output/server/_ssr/insights-D0FWpfOS.mjs +0 -10
  316. package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
  317. package/.output/server/_ssr/insights-recovery-rpc-BE9YsK_T.mjs +0 -37
  318. package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
  319. package/.output/server/_ssr/insights-usage-rpc-BE_aCnea.mjs +0 -184
  320. package/.output/server/_ssr/insights-usage-rpc-CtAolz-t.mjs +0 -24
  321. package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
  322. package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
  323. package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
  324. package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
  325. package/.output/server/_ssr/insights_.distribution-Cp0kf1vW.mjs +0 -10
  326. package/.output/server/_ssr/installations-Bhyiwx8G.mjs +0 -29
  327. package/.output/server/_ssr/routes-xBkI2Fmt.mjs +0 -37
  328. package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
  329. package/.output/server/_ssr/server-BdkuOWe8.mjs +0 -7468
  330. package/.output/server/_ssr/signing-CQ70saFm.mjs +0 -133
  331. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  332. package/.output/server/_tanstack-start-manifest_v-CrBsoKQQ.mjs +0 -125
  333. package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
  334. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
  335. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  336. package/src/lib/bundle-signing-api.ts +0 -14
  337. package/src/lib/bundle-signing-rpc.ts +0 -16
  338. package/src/lib/server/bundle-signing.server.ts +0 -23
  339. package/src/lib/server/deleteBundle.ts +0 -145
  340. package/src/lib/server/insightsHistory.ts +0 -52
  341. package/src/lib/server/insightsRecovery.ts +0 -187
  342. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,325 @@
1
+ import { AlertTriangle, ArrowRight, Loader2, Upload } from "lucide-react";
2
+ import { type FormEvent, useId, useState } from "react";
3
+ import { toast } from "sonner";
4
+
5
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog";
15
+ import {
16
+ Field,
17
+ FieldDescription,
18
+ FieldError,
19
+ FieldLabel,
20
+ } from "@/components/ui/field";
21
+ import { Input } from "@/components/ui/input";
22
+ import { usePublishRemoteConfigMutation } from "@/lib/remote-config-api";
23
+ import {
24
+ describeConditionChange,
25
+ describeParameterChange,
26
+ type FieldChange,
27
+ type RemoteConfigTemplate,
28
+ summarizeChanges,
29
+ type TemplateChanges,
30
+ } from "@/lib/remote-config-draft";
31
+ import type { ConsolePublishResult } from "@/lib/remote-config-rpc";
32
+
33
+ import { useDateTimeText } from "./useDateTimeText";
34
+
35
+ /** A changed or added key, and each of its fields: before to after, or as added. */
36
+ function ChangedKey({
37
+ name,
38
+ fields,
39
+ }: {
40
+ readonly name: string;
41
+ readonly fields: readonly FieldChange[];
42
+ }) {
43
+ return (
44
+ <li className="min-w-0">
45
+ <p className="break-words font-mono">{name}</p>
46
+ {fields.length === 0 ? null : (
47
+ <ul className="mt-0.5 flex flex-col gap-0.5">
48
+ {fields.map((change) => (
49
+ <li
50
+ className="flex min-w-0 flex-wrap items-center gap-x-1.5 text-muted-foreground"
51
+ key={change.field}
52
+ >
53
+ <span>{change.field}</span>
54
+ {change.before === null ? null : (
55
+ <>
56
+ <span
57
+ className="max-w-full truncate font-mono line-through decoration-muted-foreground/60"
58
+ title={change.before}
59
+ >
60
+ {change.before}
61
+ </span>
62
+ <ArrowRight aria-hidden="true" className="size-3 shrink-0" />
63
+ <span className="sr-only">to</span>
64
+ </>
65
+ )}
66
+ <span
67
+ className="max-w-full truncate font-mono text-foreground"
68
+ title={change.after}
69
+ >
70
+ {change.after}
71
+ </span>
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ )}
76
+ </li>
77
+ );
78
+ }
79
+
80
+ function ChangeList({
81
+ label,
82
+ changes,
83
+ describe,
84
+ }: {
85
+ readonly label: string;
86
+ readonly changes: TemplateChanges;
87
+ /** The fields a changed key changes, or an added key sets. */
88
+ readonly describe: (key: string) => readonly FieldChange[];
89
+ }) {
90
+ const rows = [
91
+ ["Added", changes.added],
92
+ ["Changed", changes.changed],
93
+ ["Removed", changes.removed],
94
+ ] as const;
95
+ if (rows.every(([, keys]) => keys.length === 0)) return null;
96
+ return (
97
+ <div>
98
+ <p className="text-xs font-medium">{label}</p>
99
+ <dl className="mt-1 grid grid-cols-[5rem_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs">
100
+ {rows.map(([title, keys]) =>
101
+ keys.length === 0 ? null : (
102
+ <div className="contents" key={title}>
103
+ <dt className="text-muted-foreground">{title}</dt>
104
+ <dd className="min-w-0">
105
+ {title !== "Removed" ? (
106
+ <ul className="flex flex-col gap-1.5">
107
+ {keys.map((key) => (
108
+ <ChangedKey fields={describe(key)} key={key} name={key} />
109
+ ))}
110
+ </ul>
111
+ ) : (
112
+ <span className="break-words font-mono">
113
+ {keys.join(", ")}
114
+ </span>
115
+ )}
116
+ </dd>
117
+ </div>
118
+ ),
119
+ )}
120
+ </dl>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ /** Publishes the draft as the next version, after a summary of what changes. */
126
+ export function PublishDialog({
127
+ open,
128
+ onOpenChange,
129
+ base,
130
+ baseVersion,
131
+ draft,
132
+ onPublished,
133
+ onLoadLatest,
134
+ onPublishOver,
135
+ }: {
136
+ readonly open: boolean;
137
+ readonly onOpenChange: (open: boolean) => void;
138
+ /** The template the draft started from. */
139
+ readonly base: RemoteConfigTemplate;
140
+ readonly baseVersion: number;
141
+ readonly draft: RemoteConfigTemplate;
142
+ readonly onPublished: () => void;
143
+ /** Drops the draft for the version someone else published. */
144
+ readonly onLoadLatest: () => void;
145
+ /** Keeps the draft and publishes it after `version`. */
146
+ readonly onPublishOver: (version: number) => void;
147
+ }) {
148
+ const id = useId();
149
+ const publish = usePublishRemoteConfigMutation();
150
+ const [description, setDescription] = useState("");
151
+ const [result, setResult] = useState<ConsolePublishResult | null>(null);
152
+ const { dateTimeText } = useDateTimeText();
153
+ const changes = summarizeChanges(base, draft);
154
+ const describeParameter = (key: string) => {
155
+ const before = Object.hasOwn(base.parameters, key)
156
+ ? base.parameters[key]!
157
+ : null;
158
+ const after = Object.hasOwn(draft.parameters, key)
159
+ ? draft.parameters[key]!
160
+ : null;
161
+ return after === null ? [] : describeParameterChange(before, after);
162
+ };
163
+ const describeCondition = (name: string) => {
164
+ const before = base.conditions.find((it) => it.name === name) ?? null;
165
+ const after = draft.conditions.find((it) => it.name === name) ?? null;
166
+ return after === null
167
+ ? []
168
+ : describeConditionChange(before, after, dateTimeText);
169
+ };
170
+ const tooLong = description.trim().length > 256;
171
+
172
+ const close = (next: boolean) => {
173
+ if (publish.isPending) return;
174
+ onOpenChange(next);
175
+ if (!next) {
176
+ setResult(null);
177
+ publish.reset();
178
+ }
179
+ };
180
+
181
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
182
+ event.preventDefault();
183
+ if (tooLong) return;
184
+ try {
185
+ const outcome = await publish.mutateAsync({
186
+ template: draft,
187
+ baseVersion,
188
+ ...(description.trim().length === 0
189
+ ? {}
190
+ : { description: description.trim() }),
191
+ });
192
+ setResult(outcome);
193
+ if (outcome.status === "published") {
194
+ toast.success(`Published version ${outcome.version.version}`);
195
+ setDescription("");
196
+ setResult(null);
197
+ onOpenChange(false);
198
+ onPublished();
199
+ }
200
+ } catch (error) {
201
+ toast.error(error instanceof Error ? error.message : "Unable to publish");
202
+ }
203
+ };
204
+
205
+ return (
206
+ <Dialog onOpenChange={close} open={open}>
207
+ <DialogContent className="max-h-[calc(100svh-2rem)] overflow-y-auto sm:max-w-lg">
208
+ <form className="flex flex-col gap-4" noValidate onSubmit={submit}>
209
+ <DialogHeader>
210
+ <DialogTitle>Publish changes</DialogTitle>
211
+ <DialogDescription>
212
+ This becomes version {baseVersion + 1}. Devices get its values on
213
+ their next fetch; earlier versions stay available to roll back to.
214
+ </DialogDescription>
215
+ </DialogHeader>
216
+ <div className="flex flex-col gap-3 rounded-lg border p-3">
217
+ <ChangeList
218
+ changes={changes.parameters}
219
+ describe={describeParameter}
220
+ label="Parameters"
221
+ />
222
+ <ChangeList
223
+ changes={changes.conditions}
224
+ describe={describeCondition}
225
+ label="Conditions"
226
+ />
227
+ {changes.reordered ? (
228
+ <p className="text-xs text-muted-foreground">
229
+ Condition priority changed.
230
+ </p>
231
+ ) : null}
232
+ </div>
233
+ {result?.status === "invalid" ? (
234
+ <Alert variant="destructive">
235
+ <AlertTriangle />
236
+ <AlertTitle>The server refused the template</AlertTitle>
237
+ <AlertDescription>
238
+ <ul className="list-disc pl-4">
239
+ {result.issues.slice(0, 6).map((issue) => (
240
+ <li key={`${issue.path}:${issue.message}`}>
241
+ {issue.path === "" ? "" : `${issue.path}: `}
242
+ {issue.message}
243
+ </li>
244
+ ))}
245
+ </ul>
246
+ </AlertDescription>
247
+ </Alert>
248
+ ) : null}
249
+ {result?.status === "conflict" ? (
250
+ <Alert>
251
+ <AlertTriangle />
252
+ <AlertTitle>
253
+ Version {result.currentVersion} was published while you edited
254
+ </AlertTitle>
255
+ <AlertDescription>
256
+ Load it to review what changed, or publish yours after it,
257
+ replacing its values.
258
+ </AlertDescription>
259
+ <div className="col-span-full mt-2 flex flex-wrap gap-2">
260
+ <Button
261
+ onClick={() => {
262
+ close(false);
263
+ onLoadLatest();
264
+ }}
265
+ size="sm"
266
+ type="button"
267
+ variant="outline"
268
+ >
269
+ Load version {result.currentVersion}
270
+ </Button>
271
+ <Button
272
+ onClick={() => {
273
+ onPublishOver(result.currentVersion);
274
+ setResult(null);
275
+ }}
276
+ size="sm"
277
+ type="button"
278
+ variant="ghost"
279
+ >
280
+ Publish mine after it
281
+ </Button>
282
+ </div>
283
+ </Alert>
284
+ ) : null}
285
+ <Field data-invalid={tooLong || undefined}>
286
+ <FieldLabel htmlFor={`${id}-description`}>Description</FieldLabel>
287
+ <Input
288
+ autoComplete="off"
289
+ className="min-h-11 sm:min-h-9"
290
+ id={`${id}-description`}
291
+ onChange={(event) => setDescription(event.target.value)}
292
+ placeholder="What changed and why"
293
+ value={description}
294
+ />
295
+ <FieldDescription>Shown in the version history.</FieldDescription>
296
+ <FieldError>
297
+ {tooLong ? "Use 256 characters or fewer." : null}
298
+ </FieldError>
299
+ </Field>
300
+ <DialogFooter>
301
+ <Button
302
+ disabled={publish.isPending}
303
+ onClick={() => close(false)}
304
+ type="button"
305
+ variant="outline"
306
+ >
307
+ Cancel
308
+ </Button>
309
+ <Button
310
+ disabled={publish.isPending || result?.status === "conflict"}
311
+ type="submit"
312
+ >
313
+ {publish.isPending ? (
314
+ <Loader2 className="animate-spin" data-icon="inline-start" />
315
+ ) : (
316
+ <Upload data-icon="inline-start" />
317
+ )}
318
+ Publish version {baseVersion + 1}
319
+ </Button>
320
+ </DialogFooter>
321
+ </form>
322
+ </DialogContent>
323
+ </Dialog>
324
+ );
325
+ }
@@ -0,0 +1,353 @@
1
+ import dayjs from "dayjs";
2
+ import relativeTime from "dayjs/plugin/relativeTime";
3
+ import {
4
+ AlertTriangle,
5
+ History,
6
+ RefreshCw,
7
+ SlidersHorizontal,
8
+ Undo2,
9
+ Upload,
10
+ } from "lucide-react";
11
+ import { useEffect, useState } from "react";
12
+
13
+ import {
14
+ Alert,
15
+ AlertAction,
16
+ AlertDescription,
17
+ AlertTitle,
18
+ } from "@/components/ui/alert";
19
+ import { Badge } from "@/components/ui/badge";
20
+ import { Button } from "@/components/ui/button";
21
+ import { SidebarTrigger } from "@/components/ui/sidebar";
22
+ import { Skeleton } from "@/components/ui/skeleton";
23
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
24
+ import { useRemoteConfigQuery } from "@/lib/remote-config-api";
25
+ import {
26
+ isSameTemplate,
27
+ moveCondition,
28
+ readStoredDraft,
29
+ type RemoteConfigDraft,
30
+ removeCondition,
31
+ removeParameter,
32
+ type RemoteConfigTemplate,
33
+ storeDraft,
34
+ upsertCondition,
35
+ upsertParameter,
36
+ } from "@/lib/remote-config-draft";
37
+
38
+ import { ConditionsCard } from "./ConditionsCard";
39
+ import { ParametersCard } from "./ParametersCard";
40
+ import { PreviewCard } from "./PreviewCard";
41
+ import { PublishDialog } from "./PublishDialog";
42
+ import { VersionsCard } from "./VersionsCard";
43
+
44
+ dayjs.extend(relativeTime);
45
+
46
+ export type RemoteConfigTab =
47
+ | "parameters"
48
+ | "conditions"
49
+ | "preview"
50
+ | "versions";
51
+
52
+ export const REMOTE_CONFIG_TABS: readonly RemoteConfigTab[] = [
53
+ "parameters",
54
+ "conditions",
55
+ "preview",
56
+ "versions",
57
+ ];
58
+
59
+ /**
60
+ * Remote Config: edit the parameters and conditions of a draft, preview what
61
+ * a device gets, publish the draft as a version, and roll back to an earlier
62
+ * one.
63
+ */
64
+ export function RemoteConfigPage({
65
+ tab,
66
+ onTabChange,
67
+ }: {
68
+ readonly tab: RemoteConfigTab;
69
+ readonly onTabChange: (tab: RemoteConfigTab) => void;
70
+ }) {
71
+ const active = useRemoteConfigQuery();
72
+ const [draft, setDraft] = useState<RemoteConfigDraft | null>(null);
73
+ const [restoreChecked, setRestoreChecked] = useState(false);
74
+ const [restored, setRestored] = useState(false);
75
+ const [publishing, setPublishing] = useState(false);
76
+ const published = active.data;
77
+ const template = draft?.template ?? published?.template;
78
+ const dirty =
79
+ draft !== null &&
80
+ published !== undefined &&
81
+ !isSameTemplate(draft.template, published.template);
82
+ const stale =
83
+ dirty && published !== undefined && draft.baseVersion !== published.version;
84
+
85
+ // A reload brings the draft back from this browser, onto the version and
86
+ // template it started from.
87
+ useEffect(() => {
88
+ if (restoreChecked || published === undefined) return;
89
+ setRestoreChecked(true);
90
+ const stored = readStoredDraft(published);
91
+ if (stored === null) return;
92
+ setDraft(stored);
93
+ setRestored(true);
94
+ }, [published, restoreChecked]);
95
+
96
+ // The browser keeps every edit; when it refuses, leaving the page would
97
+ // lose the draft, so it asks first.
98
+ useEffect(() => {
99
+ if (draft === null || !dirty || storeDraft(draft)) return;
100
+ const warn = (event: BeforeUnloadEvent) => event.preventDefault();
101
+ window.addEventListener("beforeunload", warn);
102
+ return () => window.removeEventListener("beforeunload", warn);
103
+ }, [draft, dirty]);
104
+
105
+ const clearDraft = () => {
106
+ setDraft(null);
107
+ setRestored(false);
108
+ storeDraft(null);
109
+ };
110
+
111
+ const edit = (
112
+ change: (template: RemoteConfigTemplate) => RemoteConfigTemplate,
113
+ ) => {
114
+ if (published === undefined || template === undefined) return;
115
+ setRestored(false);
116
+ setDraft({
117
+ baseVersion: draft?.baseVersion ?? published.version,
118
+ base: draft?.base ?? published.template,
119
+ template: change(template),
120
+ });
121
+ };
122
+
123
+ const counts = {
124
+ parameters:
125
+ template === undefined ? null : Object.keys(template.parameters).length,
126
+ conditions: template?.conditions.length ?? null,
127
+ };
128
+
129
+ return (
130
+ <div className="flex h-svh min-h-0 flex-col">
131
+ <header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b bg-background px-3 py-3 sm:min-h-12 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
132
+ <SidebarTrigger className="-ml-1 size-11 lg:size-7" />
133
+ <div className="flex items-center gap-1.5">
134
+ <SlidersHorizontal
135
+ aria-hidden="true"
136
+ className="size-3.5 text-muted-foreground"
137
+ />
138
+ <h1 className="text-sm font-medium">Remote Config</h1>
139
+ </div>
140
+ {published === undefined ? null : (
141
+ <p className="text-xs text-muted-foreground">
142
+ {published.version === 0 ? (
143
+ "Nothing published yet"
144
+ ) : (
145
+ <>
146
+ Version {published.version}
147
+ {published.updatedAtMs === null ? null : (
148
+ <>
149
+ {" · published "}
150
+ <time
151
+ dateTime={new Date(published.updatedAtMs).toISOString()}
152
+ title={dayjs(published.updatedAtMs).format(
153
+ "YYYY/MM/DD HH:mm:ss",
154
+ )}
155
+ >
156
+ {dayjs(published.updatedAtMs).fromNow()}
157
+ </time>
158
+ </>
159
+ )}
160
+ </>
161
+ )}
162
+ </p>
163
+ )}
164
+ {dirty ? (
165
+ <div className="ml-auto flex items-center gap-2">
166
+ <Badge className="hidden sm:inline-flex" variant="secondary">
167
+ Unpublished changes
168
+ </Badge>
169
+ <Button
170
+ className="min-h-11 lg:min-h-7"
171
+ onClick={clearDraft}
172
+ variant="ghost"
173
+ >
174
+ <Undo2 data-icon="inline-start" />
175
+ Discard
176
+ </Button>
177
+ <Button
178
+ className="min-h-11 lg:min-h-7"
179
+ onClick={() => setPublishing(true)}
180
+ >
181
+ <Upload data-icon="inline-start" />
182
+ Publish changes
183
+ </Button>
184
+ </div>
185
+ ) : null}
186
+ </header>
187
+
188
+ <div className="min-h-0 min-w-0 flex-1 overflow-y-auto bg-muted/5 p-3 sm:p-6">
189
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
190
+ {active.isError ? (
191
+ <Alert variant="destructive">
192
+ <AlertTriangle />
193
+ <AlertTitle>Remote Config couldn't be loaded</AlertTitle>
194
+ <AlertDescription>
195
+ Check the connection and try again.
196
+ </AlertDescription>
197
+ <AlertAction>
198
+ <Button
199
+ onClick={() => void active.refetch()}
200
+ size="xs"
201
+ variant="outline"
202
+ >
203
+ <RefreshCw data-icon="inline-start" />
204
+ Retry
205
+ </Button>
206
+ </AlertAction>
207
+ </Alert>
208
+ ) : null}
209
+ {restored && dirty && draft !== null ? (
210
+ <Alert>
211
+ <History />
212
+ <AlertTitle>Your unpublished changes are back</AlertTitle>
213
+ <AlertDescription>
214
+ This browser kept them from your last visit, on version{" "}
215
+ {draft.baseVersion}.
216
+ </AlertDescription>
217
+ <AlertAction>
218
+ <Button onClick={clearDraft} size="xs" variant="outline">
219
+ Discard them
220
+ </Button>
221
+ </AlertAction>
222
+ </Alert>
223
+ ) : null}
224
+ {stale && draft !== null && published !== undefined ? (
225
+ <Alert>
226
+ <AlertTriangle />
227
+ <AlertTitle>
228
+ Version {published.version} was published while you edited
229
+ </AlertTitle>
230
+ <AlertDescription>
231
+ Your draft started from version {draft.baseVersion}. Discard it
232
+ to review version {published.version}, or publish yours after
233
+ it.
234
+ </AlertDescription>
235
+ <AlertAction>
236
+ <Button onClick={clearDraft} size="xs" variant="outline">
237
+ Load version {published.version}
238
+ </Button>
239
+ </AlertAction>
240
+ </Alert>
241
+ ) : null}
242
+ <Tabs
243
+ onValueChange={(value) => onTabChange(value as RemoteConfigTab)}
244
+ value={tab}
245
+ >
246
+ <TabsList
247
+ aria-label="Remote Config views"
248
+ className="max-w-full group-data-horizontal/tabs:h-auto"
249
+ >
250
+ {(
251
+ [
252
+ ["parameters", "Parameters", counts.parameters],
253
+ ["conditions", "Conditions", counts.conditions],
254
+ ["preview", "Preview", null],
255
+ ["versions", "Versions", null],
256
+ ] as const
257
+ ).map(([value, label, count]) => (
258
+ <TabsTrigger
259
+ className="min-h-11 flex-none px-3 lg:min-h-7"
260
+ key={value}
261
+ value={value}
262
+ >
263
+ {label}
264
+ {count === null ? null : (
265
+ <span className="tabular-nums text-muted-foreground">
266
+ {count}
267
+ </span>
268
+ )}
269
+ </TabsTrigger>
270
+ ))}
271
+ </TabsList>
272
+ {template === undefined || published === undefined ? (
273
+ active.isError ? null : (
274
+ <div className="flex flex-col gap-3 pt-2">
275
+ <span className="sr-only">Loading Remote Config</span>
276
+ <Skeleton className="h-14 w-full" />
277
+ <Skeleton className="h-14 w-full" />
278
+ <Skeleton className="h-14 w-full" />
279
+ </div>
280
+ )
281
+ ) : (
282
+ <>
283
+ <TabsContent value="parameters">
284
+ <ParametersCard
285
+ onAddCondition={(condition) =>
286
+ edit((current) =>
287
+ upsertCondition(current, null, condition),
288
+ )
289
+ }
290
+ onRemoveParameter={(key) =>
291
+ edit((current) => removeParameter(current, key))
292
+ }
293
+ onSaveParameter={(previousKey, key, parameter) =>
294
+ edit((current) =>
295
+ upsertParameter(current, previousKey, key, parameter),
296
+ )
297
+ }
298
+ template={template}
299
+ />
300
+ </TabsContent>
301
+ <TabsContent value="conditions">
302
+ <ConditionsCard
303
+ onMoveCondition={(index, offset) =>
304
+ edit((current) => moveCondition(current, index, offset))
305
+ }
306
+ onRemoveCondition={(name) =>
307
+ edit((current) => removeCondition(current, name))
308
+ }
309
+ onSaveCondition={(previousName, condition) =>
310
+ edit((current) =>
311
+ upsertCondition(current, previousName, condition),
312
+ )
313
+ }
314
+ template={template}
315
+ />
316
+ </TabsContent>
317
+ <TabsContent value="preview">
318
+ <PreviewCard isDraft={dirty} template={template} />
319
+ </TabsContent>
320
+ <TabsContent value="versions">
321
+ <VersionsCard
322
+ activeVersion={published.version}
323
+ hasDraft={dirty}
324
+ onRolledBack={clearDraft}
325
+ onStartEditing={() => onTabChange("parameters")}
326
+ />
327
+ </TabsContent>
328
+ </>
329
+ )}
330
+ </Tabs>
331
+ </div>
332
+ </div>
333
+ {published !== undefined && draft !== null ? (
334
+ <PublishDialog
335
+ base={published.template}
336
+ baseVersion={draft.baseVersion}
337
+ draft={draft.template}
338
+ onLoadLatest={clearDraft}
339
+ onOpenChange={setPublishing}
340
+ onPublishOver={(version) =>
341
+ setDraft({
342
+ ...draft,
343
+ baseVersion: version,
344
+ base: published.template,
345
+ })
346
+ }
347
+ onPublished={clearDraft}
348
+ open={publishing}
349
+ />
350
+ ) : null}
351
+ </div>
352
+ );
353
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ type DateTimeText,
3
+ describeRule,
4
+ type RemoteConfigCondition,
5
+ } from "@/lib/remote-config-draft";
6
+
7
+ /** A condition's rules in a few words, unless its name already says them. */
8
+ export function RuleSummary({
9
+ condition,
10
+ dateTimeText,
11
+ }: {
12
+ readonly condition: RemoteConfigCondition;
13
+ readonly dateTimeText: DateTimeText;
14
+ }) {
15
+ const text = condition.rules
16
+ .map((rule) => describeRule(rule, dateTimeText))
17
+ .join(" · ");
18
+ return text === condition.name ? null : (
19
+ <p className="text-xs text-muted-foreground">{text}</p>
20
+ );
21
+ }