@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,247 @@
1
+ import {
2
+ ArrowDown,
3
+ ArrowUp,
4
+ GitBranch,
5
+ Pencil,
6
+ Plus,
7
+ Trash2,
8
+ } from "lucide-react";
9
+ import { type MouseEvent, useState } from "react";
10
+
11
+ import {
12
+ AlertDialog,
13
+ AlertDialogAction,
14
+ AlertDialogCancel,
15
+ AlertDialogContent,
16
+ AlertDialogDescription,
17
+ AlertDialogFooter,
18
+ AlertDialogHeader,
19
+ AlertDialogTitle,
20
+ } from "@/components/ui/alert-dialog";
21
+ import { Button } from "@/components/ui/button";
22
+ import {
23
+ Card,
24
+ CardContent,
25
+ CardDescription,
26
+ CardHeader,
27
+ CardTitle,
28
+ } from "@/components/ui/card";
29
+ import {
30
+ Empty,
31
+ EmptyContent,
32
+ EmptyDescription,
33
+ EmptyHeader,
34
+ EmptyMedia,
35
+ EmptyTitle,
36
+ } from "@/components/ui/empty";
37
+ import {
38
+ parametersUsing,
39
+ type RemoteConfigCondition,
40
+ type RemoteConfigTemplate,
41
+ } from "@/lib/remote-config-draft";
42
+ import { useDialogTarget } from "@/lib/use-dialog-target";
43
+
44
+ import { ConditionDialog } from "./ConditionDialog";
45
+ import { RuleSummary } from "./RuleSummary";
46
+ import { useDateTimeText } from "./useDateTimeText";
47
+
48
+ /** The draft's conditions in priority order: the first match decides a value. */
49
+ export function ConditionsCard({
50
+ template,
51
+ onSaveCondition,
52
+ onRemoveCondition,
53
+ onMoveCondition,
54
+ }: {
55
+ readonly template: RemoteConfigTemplate;
56
+ readonly onSaveCondition: (
57
+ previousName: string | null,
58
+ condition: RemoteConfigCondition,
59
+ ) => void;
60
+ readonly onRemoveCondition: (name: string) => void;
61
+ readonly onMoveCondition: (index: number, offset: -1 | 1) => void;
62
+ }) {
63
+ // What each dialog shows is taken when it opens, so it holds still while
64
+ // it animates closed after the draft changed.
65
+ const [editing, setEditing] = useState<{
66
+ readonly condition: RemoteConfigCondition | null;
67
+ } | null>(null);
68
+ const [removing, setRemoving] = useState<{
69
+ readonly name: string;
70
+ readonly users: readonly string[];
71
+ } | null>(null);
72
+ const shownEditing = useDialogTarget(editing);
73
+ const shownRemoving = useDialogTarget(removing);
74
+ const { dateTimeText } = useDateTimeText();
75
+ const { conditions } = template;
76
+ const editingName = shownEditing?.condition?.name ?? null;
77
+
78
+ const remove = (name: string) => {
79
+ const users = parametersUsing(template, name);
80
+ if (users.length === 0) onRemoveCondition(name);
81
+ else setRemoving({ name, users });
82
+ };
83
+
84
+ return (
85
+ <Card className="overflow-hidden shadow-sm">
86
+ <CardHeader className="flex-row flex-wrap items-center justify-between gap-3 space-y-0 border-b px-4 py-3 sm:px-6">
87
+ <div className="min-w-0">
88
+ <CardTitle className="text-sm">
89
+ <h2>Conditions</h2>
90
+ </CardTitle>
91
+ <CardDescription className="text-xs">
92
+ Listed by priority: when several match a device, the first wins.
93
+ </CardDescription>
94
+ </div>
95
+ <Button
96
+ className="min-h-11 sm:min-h-7"
97
+ onClick={() => setEditing({ condition: null })}
98
+ >
99
+ <Plus data-icon="inline-start" />
100
+ Add condition
101
+ </Button>
102
+ </CardHeader>
103
+ <CardContent className="p-0">
104
+ {conditions.length === 0 ? (
105
+ <Empty className="py-10">
106
+ <EmptyHeader>
107
+ <EmptyMedia variant="icon">
108
+ <GitBranch />
109
+ </EmptyMedia>
110
+ <EmptyTitle>No conditions</EmptyTitle>
111
+ <EmptyDescription>
112
+ Add a condition to give some devices other values: a platform,
113
+ channel, app version range, cohort, or a share of installs.
114
+ </EmptyDescription>
115
+ </EmptyHeader>
116
+ <EmptyContent>
117
+ <Button
118
+ className="min-h-11 sm:min-h-7"
119
+ onClick={() => setEditing({ condition: null })}
120
+ >
121
+ <Plus data-icon="inline-start" />
122
+ Add condition
123
+ </Button>
124
+ </EmptyContent>
125
+ </Empty>
126
+ ) : (
127
+ <ol aria-label="Conditions" className="divide-y">
128
+ {conditions.map((condition, index) => {
129
+ const users = parametersUsing(template, condition.name);
130
+ return (
131
+ <li
132
+ className="grid gap-3 px-4 py-3 sm:px-6 md:grid-cols-[auto_minmax(0,1fr)_auto] md:items-center"
133
+ key={condition.name}
134
+ >
135
+ <span
136
+ aria-label={`Priority ${index + 1}`}
137
+ className="flex size-6 items-center justify-center rounded-md bg-muted text-xs font-medium tabular-nums"
138
+ >
139
+ {index + 1}
140
+ </span>
141
+ <div className="min-w-0">
142
+ <p className="truncate text-sm font-medium">
143
+ {condition.name}
144
+ </p>
145
+ <RuleSummary
146
+ condition={condition}
147
+ dateTimeText={dateTimeText}
148
+ />
149
+ <p className="mt-1 text-xs text-muted-foreground">
150
+ {users.length === 0
151
+ ? "No parameter uses it yet"
152
+ : `Sets ${users.length === 1 ? "1 parameter" : `${users.length} parameters`}: ${users.slice(0, 3).join(", ")}${users.length > 3 ? "…" : ""}`}
153
+ </p>
154
+ </div>
155
+ <div className="flex flex-wrap gap-1 md:justify-end">
156
+ <Button
157
+ aria-label={`Move ${condition.name} up`}
158
+ className="size-11 md:size-7"
159
+ disabled={index === 0}
160
+ onClick={() => onMoveCondition(index, -1)}
161
+ size="icon"
162
+ variant="ghost"
163
+ >
164
+ <ArrowUp />
165
+ </Button>
166
+ <Button
167
+ aria-label={`Move ${condition.name} down`}
168
+ className="size-11 md:size-7"
169
+ disabled={index === conditions.length - 1}
170
+ onClick={() => onMoveCondition(index, 1)}
171
+ size="icon"
172
+ variant="ghost"
173
+ >
174
+ <ArrowDown />
175
+ </Button>
176
+ <Button
177
+ aria-label={`Edit ${condition.name}`}
178
+ className="min-h-11 md:min-h-7"
179
+ onClick={() => setEditing({ condition })}
180
+ size="sm"
181
+ variant="outline"
182
+ >
183
+ <Pencil data-icon="inline-start" />
184
+ Edit
185
+ </Button>
186
+ <Button
187
+ aria-label={`Delete ${condition.name}`}
188
+ className="size-11 md:size-7"
189
+ onClick={() => remove(condition.name)}
190
+ size="icon"
191
+ variant="ghost"
192
+ >
193
+ <Trash2 />
194
+ </Button>
195
+ </div>
196
+ </li>
197
+ );
198
+ })}
199
+ </ol>
200
+ )}
201
+ </CardContent>
202
+ <ConditionDialog
203
+ condition={shownEditing?.condition ?? null}
204
+ onOpenChange={(open) => {
205
+ if (!open) setEditing(null);
206
+ }}
207
+ onSave={(condition) => onSaveCondition(editingName, condition)}
208
+ open={editing !== null}
209
+ takenNames={conditions
210
+ .map(({ name }) => name)
211
+ .filter((name) => name !== editingName)}
212
+ />
213
+ <AlertDialog
214
+ onOpenChange={(open) => {
215
+ if (!open) setRemoving(null);
216
+ }}
217
+ open={removing !== null}
218
+ >
219
+ <AlertDialogContent>
220
+ <AlertDialogHeader>
221
+ <AlertDialogTitle>Delete {shownRemoving?.name}?</AlertDialogTitle>
222
+ <AlertDialogDescription>
223
+ {shownRemoving?.users.length === 1
224
+ ? "1 parameter has a value for it"
225
+ : `${shownRemoving?.users.length} parameters have a value for it`}{" "}
226
+ ({shownRemoving?.users.join(", ")}). Those values are removed too,
227
+ so matching devices get the default value after you publish.
228
+ </AlertDialogDescription>
229
+ </AlertDialogHeader>
230
+ <AlertDialogFooter>
231
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
232
+ <AlertDialogAction
233
+ onClick={(event: MouseEvent<HTMLButtonElement>) => {
234
+ event.preventDefault();
235
+ if (removing !== null) onRemoveCondition(removing.name);
236
+ setRemoving(null);
237
+ }}
238
+ variant="destructive"
239
+ >
240
+ Delete condition
241
+ </AlertDialogAction>
242
+ </AlertDialogFooter>
243
+ </AlertDialogContent>
244
+ </AlertDialog>
245
+ </Card>
246
+ );
247
+ }
@@ -0,0 +1,426 @@
1
+ import { Plus, X } from "lucide-react";
2
+ import { type FormEvent, useId, useState } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import {
14
+ Field,
15
+ FieldDescription,
16
+ FieldError,
17
+ FieldGroup,
18
+ FieldLabel,
19
+ FieldLegend,
20
+ FieldSet,
21
+ } from "@/components/ui/field";
22
+ import { Input } from "@/components/ui/input";
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectGroup,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from "@/components/ui/select";
31
+ import {
32
+ conditionalValueFor,
33
+ parameterKeyError,
34
+ type RemoteConfigCondition,
35
+ type RemoteConfigParameter,
36
+ type RemoteConfigParameterValue,
37
+ type RemoteConfigValueType,
38
+ VALUE_TYPE_LABELS,
39
+ } from "@/lib/remote-config-draft";
40
+ import { useOpenSession } from "@/lib/use-dialog-target";
41
+
42
+ import { ConditionDialog } from "./ConditionDialog";
43
+ import { RuleSummary } from "./RuleSummary";
44
+ import { useDateTimeText } from "./useDateTimeText";
45
+ import {
46
+ initialValueText,
47
+ parameterValueError,
48
+ ValueInput,
49
+ } from "./ValueInput";
50
+
51
+ const VALUE_TYPES = Object.keys(VALUE_TYPE_LABELS) as RemoteConfigValueType[];
52
+
53
+ /** How the app reads a value of each type; JSON arrives as text to parse. */
54
+ const READ_METHODS: Readonly<Record<RemoteConfigValueType, string>> = {
55
+ STRING: "getString",
56
+ NUMBER: "getNumber",
57
+ BOOLEAN: "getBoolean",
58
+ JSON: "getString",
59
+ };
60
+
61
+ /** A value converted to another type: kept when it still fits, reset when not. */
62
+ const retype = (
63
+ value: RemoteConfigParameterValue,
64
+ valueType: RemoteConfigValueType,
65
+ ): RemoteConfigParameterValue =>
66
+ "value" in value && parameterValueError(valueType, value) !== null
67
+ ? { value: initialValueText(valueType) }
68
+ : value;
69
+
70
+ /**
71
+ * Adds or edits a parameter: its key, type, default value, and a value for
72
+ * any of the template's conditions, which it can also add on the spot.
73
+ */
74
+ export function ParameterDialog({
75
+ open,
76
+ onOpenChange,
77
+ parameterKey,
78
+ parameter,
79
+ takenKeys,
80
+ conditions,
81
+ onSave,
82
+ onAddCondition,
83
+ }: {
84
+ readonly open: boolean;
85
+ readonly onOpenChange: (open: boolean) => void;
86
+ /** The key of the parameter to edit, or null for a new one. */
87
+ readonly parameterKey: string | null;
88
+ readonly parameter: RemoteConfigParameter | null;
89
+ readonly takenKeys: readonly string[];
90
+ readonly conditions: readonly RemoteConfigCondition[];
91
+ readonly onSave: (key: string, parameter: RemoteConfigParameter) => void;
92
+ readonly onAddCondition: (condition: RemoteConfigCondition) => void;
93
+ }) {
94
+ // The form stays while the dialog animates closed, and starts over on
95
+ // each opening.
96
+ const session = useOpenSession(open);
97
+ return (
98
+ <Dialog onOpenChange={onOpenChange} open={open}>
99
+ <DialogContent className="max-h-[calc(100svh-2rem)] overflow-y-auto data-nested-dialog-open:after:absolute data-nested-dialog-open:after:inset-0 data-nested-dialog-open:after:rounded-xl data-nested-dialog-open:after:bg-background/70 sm:max-w-2xl">
100
+ <ParameterForm
101
+ conditions={conditions}
102
+ key={session}
103
+ onAddCondition={onAddCondition}
104
+ onCancel={() => onOpenChange(false)}
105
+ onSave={(key, next) => {
106
+ onSave(key, next);
107
+ onOpenChange(false);
108
+ }}
109
+ parameter={parameter}
110
+ parameterKey={parameterKey}
111
+ takenKeys={takenKeys}
112
+ />
113
+ </DialogContent>
114
+ </Dialog>
115
+ );
116
+ }
117
+
118
+ function ParameterForm({
119
+ parameterKey,
120
+ parameter,
121
+ takenKeys,
122
+ conditions,
123
+ onSave,
124
+ onCancel,
125
+ onAddCondition,
126
+ }: {
127
+ readonly parameterKey: string | null;
128
+ readonly parameter: RemoteConfigParameter | null;
129
+ readonly takenKeys: readonly string[];
130
+ readonly conditions: readonly RemoteConfigCondition[];
131
+ readonly onSave: (key: string, parameter: RemoteConfigParameter) => void;
132
+ readonly onCancel: () => void;
133
+ readonly onAddCondition: (condition: RemoteConfigCondition) => void;
134
+ }) {
135
+ const id = useId();
136
+ const { dateTimeText } = useDateTimeText();
137
+ const [key, setKey] = useState(parameterKey ?? "");
138
+ const [valueType, setValueType] = useState<RemoteConfigValueType>(
139
+ parameter?.valueType ?? "STRING",
140
+ );
141
+ const [description, setDescription] = useState(parameter?.description ?? "");
142
+ // Saved trimmed, as the server stores it.
143
+ const descriptionTooLong = description.trim().length > 256;
144
+ const [defaultValue, setDefaultValue] = useState<RemoteConfigParameterValue>(
145
+ parameter?.defaultValue ?? { value: "" },
146
+ );
147
+ const [conditionalValues, setConditionalValues] = useState<
148
+ Readonly<Record<string, RemoteConfigParameterValue>>
149
+ >(parameter?.conditionalValues ?? {});
150
+ // Values removed in this edit, so a condition added back gets its value
151
+ // again instead of an empty one.
152
+ const [removedValues, setRemovedValues] = useState<
153
+ Readonly<Record<string, RemoteConfigParameterValue>>
154
+ >({});
155
+ const [submitted, setSubmitted] = useState(false);
156
+ const [addingCondition, setAddingCondition] = useState(false);
157
+
158
+ const keyError = parameterKeyError(key.trim(), takenKeys);
159
+ const valued = conditions.filter(
160
+ ({ name }) => conditionalValueFor(conditionalValues, name) !== undefined,
161
+ );
162
+ const unvalued = conditions.filter(
163
+ ({ name }) => conditionalValueFor(conditionalValues, name) === undefined,
164
+ );
165
+ const invalid =
166
+ keyError !== null ||
167
+ descriptionTooLong ||
168
+ [defaultValue, ...Object.values(conditionalValues)].some(
169
+ (value) => parameterValueError(valueType, value) !== null,
170
+ );
171
+
172
+ const changeType = (next: RemoteConfigValueType) => {
173
+ setValueType(next);
174
+ setDefaultValue(retype(defaultValue, next));
175
+ setConditionalValues(
176
+ Object.fromEntries(
177
+ Object.entries(conditionalValues).map(([name, value]) => [
178
+ name,
179
+ retype(value, next),
180
+ ]),
181
+ ),
182
+ );
183
+ };
184
+
185
+ const addValueFor = (name: string) => {
186
+ const removed = conditionalValueFor(removedValues, name);
187
+ setConditionalValues({
188
+ ...conditionalValues,
189
+ [name]:
190
+ removed === undefined
191
+ ? { value: initialValueText(valueType) }
192
+ : retype(removed, valueType),
193
+ });
194
+ };
195
+
196
+ const removeValueFor = (name: string) => {
197
+ const { [name]: removed, ...rest } = conditionalValues;
198
+ if (removed !== undefined) {
199
+ setRemovedValues({ ...removedValues, [name]: removed });
200
+ }
201
+ setConditionalValues(rest);
202
+ };
203
+
204
+ const submit = (event: FormEvent<HTMLFormElement>) => {
205
+ event.preventDefault();
206
+ setSubmitted(true);
207
+ if (invalid) return;
208
+ const kept = Object.fromEntries(
209
+ valued.map(({ name }) => [
210
+ name,
211
+ conditionalValueFor(conditionalValues, name)!,
212
+ ]),
213
+ );
214
+ onSave(key.trim(), {
215
+ valueType,
216
+ defaultValue,
217
+ ...(Object.keys(kept).length === 0 ? {} : { conditionalValues: kept }),
218
+ ...(description.trim().length === 0
219
+ ? {}
220
+ : { description: description.trim() }),
221
+ });
222
+ };
223
+
224
+ return (
225
+ <>
226
+ <form className="flex flex-col gap-4" noValidate onSubmit={submit}>
227
+ <DialogHeader>
228
+ <DialogTitle>
229
+ {parameterKey === null ? "Add parameter" : "Edit parameter"}
230
+ </DialogTitle>
231
+ <DialogDescription>
232
+ The app reads it with{" "}
233
+ <code className="font-mono">
234
+ hotUpdater.remoteConfig.{READ_METHODS[valueType]}("
235
+ {key.trim() || "key"}")
236
+ </code>
237
+ , which is null when neither the server nor the app's defaults have
238
+ a value.
239
+ </DialogDescription>
240
+ </DialogHeader>
241
+ <FieldGroup>
242
+ <div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_11rem]">
243
+ <Field data-invalid={(submitted && keyError !== null) || undefined}>
244
+ <FieldLabel htmlFor={`${id}-key`}>Key</FieldLabel>
245
+ <Input
246
+ autoComplete="off"
247
+ className="min-h-11 font-mono sm:min-h-9"
248
+ id={`${id}-key`}
249
+ maxLength={257}
250
+ onChange={(event) => setKey(event.target.value)}
251
+ placeholder="welcome_message"
252
+ spellCheck={false}
253
+ value={key}
254
+ />
255
+ <FieldError>{submitted ? keyError : null}</FieldError>
256
+ </Field>
257
+ <Field>
258
+ <FieldLabel htmlFor={`${id}-type`}>Type</FieldLabel>
259
+ <Select
260
+ items={VALUE_TYPE_LABELS}
261
+ onValueChange={(value) => {
262
+ if (value !== null)
263
+ changeType(value as RemoteConfigValueType);
264
+ }}
265
+ value={valueType}
266
+ >
267
+ <SelectTrigger
268
+ className="min-h-11 w-full sm:min-h-9"
269
+ id={`${id}-type`}
270
+ >
271
+ <SelectValue />
272
+ </SelectTrigger>
273
+ <SelectContent>
274
+ <SelectGroup>
275
+ {VALUE_TYPES.map((type) => (
276
+ <SelectItem key={type} value={type}>
277
+ {VALUE_TYPE_LABELS[type]}
278
+ </SelectItem>
279
+ ))}
280
+ </SelectGroup>
281
+ </SelectContent>
282
+ </Select>
283
+ </Field>
284
+ </div>
285
+ <Field data-invalid={descriptionTooLong || 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="Optional: what the app does with it"
293
+ value={description}
294
+ />
295
+ <FieldError>
296
+ {descriptionTooLong ? "Use 256 characters or fewer." : null}
297
+ </FieldError>
298
+ </Field>
299
+ <ValueInput
300
+ id={`${id}-default`}
301
+ label="Default value"
302
+ onChange={setDefaultValue}
303
+ showErrors={submitted}
304
+ value={defaultValue}
305
+ valueType={valueType}
306
+ />
307
+ <FieldSet>
308
+ <FieldLegend variant="label">Conditional values</FieldLegend>
309
+ <FieldDescription>
310
+ Devices that match a condition get its value. When several match,
311
+ the first in the Conditions list wins. Removing a value keeps the
312
+ condition, and its devices get the default value.
313
+ </FieldDescription>
314
+ {valued.length > 0 ? (
315
+ <ul className="flex flex-col gap-3">
316
+ {valued.map((condition) => (
317
+ <li className="rounded-lg border p-3" key={condition.name}>
318
+ <div className="mb-2 flex items-start justify-between gap-2">
319
+ <div className="min-w-0">
320
+ <p className="truncate text-sm font-medium">
321
+ {condition.name}
322
+ </p>
323
+ <RuleSummary
324
+ condition={condition}
325
+ dateTimeText={dateTimeText}
326
+ />
327
+ </div>
328
+ <Button
329
+ aria-label={`Remove the value for ${condition.name}`}
330
+ className="min-h-11 shrink-0 text-muted-foreground sm:min-h-7"
331
+ onClick={() => removeValueFor(condition.name)}
332
+ size="sm"
333
+ type="button"
334
+ variant="ghost"
335
+ >
336
+ <X data-icon="inline-start" />
337
+ Remove value
338
+ </Button>
339
+ </div>
340
+ <ValueInput
341
+ id={`${id}-${condition.name}`}
342
+ label={
343
+ <>
344
+ Value
345
+ <span className="sr-only"> for {condition.name}</span>
346
+ </>
347
+ }
348
+ onChange={(value) =>
349
+ setConditionalValues({
350
+ ...conditionalValues,
351
+ [condition.name]: value,
352
+ })
353
+ }
354
+ showErrors={submitted}
355
+ value={
356
+ conditionalValueFor(conditionalValues, condition.name)!
357
+ }
358
+ valueType={valueType}
359
+ />
360
+ </li>
361
+ ))}
362
+ </ul>
363
+ ) : null}
364
+ <div className="flex flex-wrap gap-2">
365
+ {unvalued.length > 0 ? (
366
+ <Select
367
+ items={Object.fromEntries(
368
+ unvalued.map(({ name }) => [name, name]),
369
+ )}
370
+ onValueChange={(value) => {
371
+ if (value !== null) addValueFor(value);
372
+ }}
373
+ value={null}
374
+ >
375
+ <SelectTrigger
376
+ aria-label="Add a value for a condition"
377
+ className="min-h-11 w-full sm:min-h-9 sm:w-64"
378
+ >
379
+ <Plus aria-hidden="true" className="size-3.5" />
380
+ <SelectValue placeholder="Add a value for a condition" />
381
+ </SelectTrigger>
382
+ <SelectContent alignItemWithTrigger={false}>
383
+ <SelectGroup>
384
+ {unvalued.map(({ name }) => (
385
+ <SelectItem key={name} value={name}>
386
+ {name}
387
+ </SelectItem>
388
+ ))}
389
+ </SelectGroup>
390
+ </SelectContent>
391
+ </Select>
392
+ ) : null}
393
+ <Button
394
+ className="min-h-11 sm:min-h-9"
395
+ onClick={() => setAddingCondition(true)}
396
+ type="button"
397
+ variant="outline"
398
+ >
399
+ <Plus data-icon="inline-start" />
400
+ New condition
401
+ </Button>
402
+ </div>
403
+ </FieldSet>
404
+ </FieldGroup>
405
+ <DialogFooter>
406
+ <Button onClick={onCancel} type="button" variant="outline">
407
+ Cancel
408
+ </Button>
409
+ <Button type="submit">
410
+ {parameterKey === null ? "Add parameter" : "Save parameter"}
411
+ </Button>
412
+ </DialogFooter>
413
+ </form>
414
+ <ConditionDialog
415
+ condition={null}
416
+ onOpenChange={setAddingCondition}
417
+ onSave={(condition) => {
418
+ onAddCondition(condition);
419
+ addValueFor(condition.name);
420
+ }}
421
+ open={addingCondition}
422
+ takenNames={conditions.map(({ name }) => name)}
423
+ />
424
+ </>
425
+ );
426
+ }