@hot-updater/console 1.0.0-rc.9 → 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 (341) 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-CyMmr0O9.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-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
  74. package/.output/server/_ssr/{api-keys-CzZ7Ont4.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-DH6Bhwcz.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-BrGSjz4S.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-DDKmIrpV.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-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
  120. package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
  121. package/.output/server/_ssr/{installations-C5anhyP0.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-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
  135. package/.output/server/_ssr/{routes-DY-vqU6R.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-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
  140. package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-CdlfejmI.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-CTj6n-4F.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 +309 -211
  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 +69 -74
  158. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  159. package/src/components/NotFoundPage.tsx +7 -7
  160. package/src/components/features/FeatureSlots.tsx +103 -0
  161. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  162. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  163. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  164. package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
  165. package/src/components/features/insights/AppUsage.tsx +41 -14
  166. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  167. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  168. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  169. package/src/components/features/insights/EventDetails.tsx +390 -26
  170. package/src/components/features/insights/EventHistoryCard.tsx +111 -25
  171. package/src/components/features/insights/EventHistoryList.tsx +1 -1
  172. package/src/components/features/insights/FailureReports.tsx +407 -0
  173. package/src/components/features/insights/InsightsControls.tsx +240 -142
  174. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  175. package/src/components/features/insights/InsightsOverview.tsx +601 -403
  176. package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
  177. package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
  178. package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
  179. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  180. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  181. package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
  182. package/src/components/features/insights/RollbackButton.tsx +118 -0
  183. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  184. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  185. package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
  186. package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
  187. package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
  188. package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
  189. package/src/components/features/remote-config/ParametersCard.tsx +254 -0
  190. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  191. package/src/components/features/remote-config/PublishDialog.tsx +325 -0
  192. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  193. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  194. package/src/components/features/remote-config/ValueInput.tsx +151 -0
  195. package/src/components/features/remote-config/VersionsCard.tsx +384 -0
  196. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  197. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  198. package/src/components/ui/sidebar.tsx +22 -29
  199. package/src/index.ts +26 -19
  200. package/src/lib/api-keys-api.ts +0 -28
  201. package/src/lib/api-keys-rpc.ts +27 -38
  202. package/src/lib/api-rpc.ts +125 -288
  203. package/src/lib/api.ts +15 -134
  204. package/src/lib/bundle-activity.ts +24 -4
  205. package/src/lib/console-access.ts +20 -0
  206. package/src/lib/console-features-api.ts +68 -0
  207. package/src/lib/console-features-rpc.ts +13 -0
  208. package/src/lib/console-features.ts +98 -0
  209. package/src/lib/insights-api.ts +56 -2
  210. package/src/lib/insights-errors.ts +64 -0
  211. package/src/lib/insights-failures-rpc.ts +36 -0
  212. package/src/lib/insights-failures.ts +130 -0
  213. package/src/lib/insights-recovery-rpc.ts +13 -17
  214. package/src/lib/insights-recovery.ts +12 -36
  215. package/src/lib/insights-retention.ts +21 -0
  216. package/src/lib/insights-rpc.ts +30 -12
  217. package/src/lib/insights-search.ts +50 -4
  218. package/src/lib/insights-usage-rpc.ts +15 -9
  219. package/src/lib/insights-usage.ts +41 -3
  220. package/src/lib/insights-view.ts +34 -5
  221. package/src/lib/release-adoption-api.ts +214 -0
  222. package/src/lib/release-adoption-rpc.ts +49 -0
  223. package/src/lib/release-adoption.ts +164 -0
  224. package/src/lib/remote-config-api.ts +119 -0
  225. package/src/lib/remote-config-draft.ts +666 -0
  226. package/src/lib/remote-config-rpc.ts +196 -0
  227. package/src/lib/scroll-restoration.ts +21 -0
  228. package/src/lib/server/auth.server.ts +13 -5
  229. package/src/lib/server/bundleActivity.ts +27 -28
  230. package/src/lib/server/config.server.ts +21 -76
  231. package/src/lib/server/console-feature-apis.ts +70 -0
  232. package/src/lib/server/console-runtime.server.ts +82 -38
  233. package/src/lib/server/downloadBundle.ts +14 -15
  234. package/src/lib/server/failureReports.ts +50 -0
  235. package/src/lib/server/getBundleChildren.ts +31 -122
  236. package/src/lib/server/insightsUsage.ts +52 -113
  237. package/src/lib/server/listReleases.ts +109 -44
  238. package/src/lib/server/releaseAdoption.ts +151 -0
  239. package/src/lib/server/releaseReachability.ts +8 -8
  240. package/src/lib/server/remoteConfig.ts +75 -0
  241. package/src/lib/server/runtime.server.ts +120 -31
  242. package/src/lib/server/updateFailures.ts +66 -0
  243. package/src/lib/use-dialog-target.ts +27 -0
  244. package/src/routeTree.gen.ts +44 -44
  245. package/src/router.tsx +3 -2
  246. package/src/routes/-installations-search.ts +32 -8
  247. package/src/routes/-releases-search.ts +76 -29
  248. package/src/routes/__root.tsx +7 -1
  249. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  250. package/src/routes/api-keys.tsx +5 -2
  251. package/src/routes/index.tsx +210 -152
  252. package/src/routes/insights.tsx +91 -23
  253. package/src/routes/insights_.distribution.tsx +50 -9
  254. package/src/routes/installations.tsx +97 -40
  255. package/src/routes/remote-config.tsx +42 -0
  256. package/src/styles.css +9 -0
  257. package/src/vite-internal.ts +15 -3
  258. package/src/vite.ts +2 -0
  259. package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
  260. package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
  261. package/.output/public/assets/api-CdUyUqzR.js +0 -42
  262. package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
  263. package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
  264. package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
  265. package/.output/public/assets/card-CYHs5FZv.js +0 -1
  266. package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
  267. package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
  268. package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
  269. package/.output/public/assets/dist-BR4GzeEu.js +0 -1
  270. package/.output/public/assets/field-B3CYDpjg.js +0 -1
  271. package/.output/public/assets/index-Cb_D1D6N.js +0 -10
  272. package/.output/public/assets/info-C3y9HNoU.js +0 -1
  273. package/.output/public/assets/input-group-nLZylhWF.js +0 -1
  274. package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
  275. package/.output/public/assets/insights-DS3iO7Up.js +0 -1
  276. package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
  277. package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
  278. package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
  279. package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
  280. package/.output/public/assets/installations-CkFW4fV9.js +0 -2
  281. package/.output/public/assets/installations-DxTaIcHD.js +0 -1
  282. package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
  283. package/.output/public/assets/list-Dqt53fWZ.js +0 -1
  284. package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
  285. package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
  286. package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
  287. package/.output/public/assets/routes-Cgtvspjg.js +0 -2
  288. package/.output/public/assets/routes-O2r2UUN8.js +0 -1
  289. package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
  290. package/.output/public/assets/signing-BgspC-xP.js +0 -1
  291. package/.output/public/assets/styles-DnrWuBwd.css +0 -2
  292. package/.output/public/assets/table-BuWQgxJJ.js +0 -1
  293. package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
  294. package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
  295. package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
  296. package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
  297. package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
  298. package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
  299. package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
  300. package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
  301. package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
  302. package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
  303. package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
  304. package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
  305. package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
  306. package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
  307. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  308. package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
  309. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  310. package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
  311. package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
  312. package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
  313. package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
  314. package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
  315. package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
  316. package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
  317. package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
  318. package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
  319. package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
  320. package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
  321. package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
  322. package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
  323. package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
  324. package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
  325. package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
  326. package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
  327. package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
  328. package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
  329. package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
  330. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  331. package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
  332. package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
  333. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
  334. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  335. package/src/lib/bundle-signing-api.ts +0 -14
  336. package/src/lib/bundle-signing-rpc.ts +0 -16
  337. package/src/lib/server/bundle-signing.server.ts +0 -23
  338. package/src/lib/server/deleteBundle.ts +0 -145
  339. package/src/lib/server/insightsHistory.ts +0 -52
  340. package/src/lib/server/insightsRecovery.ts +0 -187
  341. 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
+ }