@hot-updater/console 0.36.7 → 1.0.0-rc.1

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 (262) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +1 -0
  3. package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +1 -0
  4. package/.output/public/assets/api-keys-api-DGVTTvTJ.js +1 -0
  5. package/.output/public/assets/api-keys-z16OZIP4.js +1 -0
  6. package/.output/public/assets/badge-Br_KDO0J.js +1 -0
  7. package/.output/public/assets/card-DzuEXvyJ.js +1 -0
  8. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  9. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  10. package/.output/public/assets/dialog-BeAENgez.js +1 -0
  11. package/.output/public/assets/dist-Bssh_cv0.js +1 -0
  12. package/.output/public/assets/field-Buq148-A.js +1 -0
  13. package/.output/public/assets/index-uOpQAIC4.js +10 -0
  14. package/.output/public/assets/input-group-B3p5UpyW.js +1 -0
  15. package/.output/public/assets/insights-DJTr7Ozn.js +1 -0
  16. package/.output/public/assets/insights-api-DVMOoRMS.js +1 -0
  17. package/.output/public/assets/installations-BY64iIAK.js +2 -0
  18. package/.output/public/assets/installations-D5v9i3AL.js +1 -0
  19. package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +1 -0
  20. package/.output/public/assets/preload-helper-CQ36nxok.js +1 -0
  21. package/.output/public/assets/refresh-cw-BkbfZ62O.js +1 -0
  22. package/.output/public/assets/routes-BuoI5-Cr.js +2 -0
  23. package/.output/public/assets/routes-C8fvgETO.js +1 -0
  24. package/.output/public/assets/search-qsIDvS2I.js +1 -0
  25. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  26. package/.output/public/assets/signing-mJD88Y4C.js +1 -0
  27. package/.output/public/assets/styles-BZIG7t42.css +2 -0
  28. package/.output/public/assets/table-RamFEclP.js +1 -0
  29. package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +42 -0
  30. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  31. package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +1 -0
  32. package/.output/server/__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs +146 -0
  33. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  34. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  35. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  36. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  37. package/.output/server/_libs/d3-array.mjs +231 -0
  38. package/.output/server/_libs/d3-color.mjs +330 -0
  39. package/.output/server/_libs/d3-format.mjs +252 -0
  40. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  41. package/.output/server/_libs/d3-path.mjs +77 -0
  42. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  43. package/.output/server/_libs/d3-shape.mjs +818 -0
  44. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  45. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  46. package/.output/server/_libs/eventemitter3.mjs +236 -0
  47. package/.output/server/_libs/immer.mjs +532 -0
  48. package/.output/server/_libs/lucide-react.mjs +141 -13
  49. package/.output/server/_libs/next-themes.mjs +2 -2
  50. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  51. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  52. package/.output/server/_libs/sonner.mjs +2 -2
  53. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  54. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  55. package/.output/server/_runtime.mjs +10 -1
  56. package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +288 -0
  57. package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +13 -0
  58. package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +15 -0
  59. package/.output/server/_ssr/api-keys-CpBqHpuC.mjs +300 -0
  60. package/.output/server/_ssr/api-keys-api-Czkp57UV.mjs +60 -0
  61. package/.output/server/_ssr/api-keys-rpc-BTtMDSoR.mjs +136 -0
  62. package/.output/server/_ssr/api-rpc-BEX8ISai.mjs +1468 -0
  63. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  64. package/.output/server/_ssr/auth-rpc-BFMEC-TT.mjs +24 -0
  65. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +47 -0
  66. package/.output/server/_ssr/badge-CpWXQwyF.mjs +26 -0
  67. package/.output/server/_ssr/bundle-signing-rpc-BbCFUQSu.mjs +16 -0
  68. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  69. package/.output/server/_ssr/card-Bsc8mRis.mjs +86 -0
  70. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  71. package/.output/server/_ssr/config.server-DQYEr_5X.mjs +361 -0
  72. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  73. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  74. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-DhfrxEjg.mjs} +68 -50
  75. package/.output/server/_ssr/databaseRows-QlCYLi0H.mjs +166 -0
  76. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  77. package/.output/server/_ssr/dialog-C0vKUp8R.mjs +170 -0
  78. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  79. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  80. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  81. package/.output/server/_ssr/field-DASCh-5d.mjs +74 -0
  82. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  83. package/.output/server/_ssr/input-group-etIsBUqv.mjs +288 -0
  84. package/.output/server/_ssr/insights-api-C1F7UreR.mjs +71 -0
  85. package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +96 -0
  86. package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +155 -0
  87. package/.output/server/_ssr/insights-ygT_aIfo.mjs +858 -0
  88. package/.output/server/_ssr/installations-DqRT1QTd.mjs +535 -0
  89. package/.output/server/_ssr/installations-K8TCYTYh.mjs +19 -0
  90. package/.output/server/_ssr/router-BxBEvtF1.mjs +513 -0
  91. package/.output/server/_ssr/routes-BC7rn-tf.mjs +37 -0
  92. package/.output/server/_ssr/routes-CM5KAwpB.mjs +2285 -0
  93. package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +527 -0
  94. package/.output/server/_ssr/server-CpzW--lT.mjs +7468 -0
  95. package/.output/server/_ssr/signing-jk4HANnl.mjs +134 -0
  96. package/.output/server/_ssr/ssr.mjs +4 -5740
  97. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  98. package/.output/server/_ssr/table-CU58HeI7.mjs +53 -0
  99. package/.output/server/_ssr/uuidv7-CaKuKXrV.mjs +31 -0
  100. package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +103 -0
  101. package/.output/server/index.mjs +229 -47
  102. package/README.md +29 -8
  103. package/lib/index.d.mts +35 -0
  104. package/lib/index.mjs +4 -0
  105. package/lib/vite-internal.mjs +127 -0
  106. package/lib/vite.d.mts +10 -0
  107. package/lib/vite.mjs +5 -0
  108. package/package.json +51 -29
  109. package/public/apple-touch-icon.png +0 -0
  110. package/public/favicon-16x16.png +0 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/logo.svg +1 -0
  114. package/public/manifest.json +30 -0
  115. package/public/robots.txt +3 -0
  116. package/src/components/AppSidebar.tsx +159 -0
  117. package/src/components/BundleIdDisplay.tsx +18 -0
  118. package/src/components/ChannelBadge.tsx +31 -0
  119. package/src/components/ConsoleAccessPage.tsx +122 -0
  120. package/src/components/EnabledStatusIcon.tsx +54 -0
  121. package/src/components/HashValueDisplay.tsx +85 -0
  122. package/src/components/HotUpdaterLogo.tsx +30 -0
  123. package/src/components/NotFoundPage.tsx +33 -0
  124. package/src/components/PlatformIcon.tsx +39 -0
  125. package/src/components/RolloutPercentageBadge.tsx +28 -0
  126. package/src/components/ThemeProvider.tsx +116 -0
  127. package/src/components/TimestampDisplay.tsx +41 -0
  128. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  129. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  130. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  131. package/src/components/features/bundles/BundleInsightsSummary.tsx +123 -0
  132. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  133. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  134. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  135. package/src/components/features/insights/ActivityChart.tsx +150 -0
  136. package/src/components/features/insights/BundleDistribution.tsx +133 -0
  137. package/src/components/features/insights/BundleSelector.tsx +114 -0
  138. package/src/components/features/insights/InsightsCapabilityContext.tsx +24 -0
  139. package/src/components/features/insights/InsightsControls.tsx +115 -0
  140. package/src/components/features/insights/InsightsErrorAlert.tsx +21 -0
  141. package/src/components/features/insights/InsightsOverview.tsx +208 -0
  142. package/src/components/features/insights/InstallationHistoryCard.tsx +248 -0
  143. package/src/components/features/insights/InstallationMatchesCard.tsx +132 -0
  144. package/src/components/features/insights/InstallationPageHeader.tsx +138 -0
  145. package/src/components/features/insights/InstallationPagination.tsx +48 -0
  146. package/src/components/features/insights/InstallationSearch.tsx +230 -0
  147. package/src/components/features/insights/UpdateOutcomes.tsx +147 -0
  148. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  149. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  150. package/src/components/features/signing/BundleSigningPage.tsx +139 -0
  151. package/src/components/ui/alert-dialog.tsx +185 -0
  152. package/src/components/ui/alert.tsx +76 -0
  153. package/src/components/ui/badge.tsx +49 -0
  154. package/src/components/ui/button.tsx +58 -0
  155. package/src/components/ui/card.tsx +89 -0
  156. package/src/components/ui/chart.tsx +371 -0
  157. package/src/components/ui/combobox.tsx +303 -0
  158. package/src/components/ui/dialog.tsx +156 -0
  159. package/src/components/ui/dropdown-menu.tsx +270 -0
  160. package/src/components/ui/field.tsx +235 -0
  161. package/src/components/ui/input-group.tsx +147 -0
  162. package/src/components/ui/input.tsx +19 -0
  163. package/src/components/ui/label.tsx +25 -0
  164. package/src/components/ui/progress.tsx +84 -0
  165. package/src/components/ui/select.tsx +205 -0
  166. package/src/components/ui/separator.tsx +23 -0
  167. package/src/components/ui/sheet.tsx +132 -0
  168. package/src/components/ui/sidebar.tsx +779 -0
  169. package/src/components/ui/skeleton.tsx +13 -0
  170. package/src/components/ui/slider.tsx +56 -0
  171. package/src/components/ui/sonner.tsx +45 -0
  172. package/src/components/ui/switch.tsx +25 -0
  173. package/src/components/ui/table.tsx +114 -0
  174. package/src/components/ui/textarea.tsx +18 -0
  175. package/src/components/ui/toggle-group.tsx +89 -0
  176. package/src/components/ui/toggle.tsx +43 -0
  177. package/src/components/ui/tooltip.tsx +66 -0
  178. package/src/hooks/use-mobile.ts +30 -0
  179. package/src/index.ts +47 -0
  180. package/src/lib/api-keys-api.ts +69 -0
  181. package/src/lib/api-keys-rpc.ts +87 -0
  182. package/src/lib/api-rpc.ts +478 -0
  183. package/src/lib/api.ts +444 -0
  184. package/src/lib/auth-rpc.ts +16 -0
  185. package/src/lib/bundle-signing-api.ts +14 -0
  186. package/src/lib/bundle-signing-rpc.ts +16 -0
  187. package/src/lib/constants.ts +1 -0
  188. package/src/lib/git.ts +51 -0
  189. package/src/lib/insights-api.ts +144 -0
  190. package/src/lib/insights-error.ts +19 -0
  191. package/src/lib/insights-input.ts +187 -0
  192. package/src/lib/insights-overview.ts +170 -0
  193. package/src/lib/insights-rpc.ts +218 -0
  194. package/src/lib/server/auth.server.ts +43 -0
  195. package/src/lib/server/bundle-signing.server.ts +23 -0
  196. package/src/lib/server/config.server.ts +109 -0
  197. package/src/lib/server/console-runtime.server.ts +61 -0
  198. package/src/lib/server/deleteBundle.ts +145 -0
  199. package/src/lib/server/downloadBundle.ts +46 -0
  200. package/src/lib/server/getBundleChildren.ts +130 -0
  201. package/src/lib/server/listReleases.ts +61 -0
  202. package/src/lib/server/releaseActivity.ts +27 -0
  203. package/src/lib/server/releaseReachability.ts +95 -0
  204. package/src/lib/server/runtime.server.ts +172 -0
  205. package/src/lib/utils.ts +9 -0
  206. package/src/logo.svg +1 -0
  207. package/src/routeTree.gen.ts +199 -0
  208. package/src/router.tsx +21 -0
  209. package/src/routes/-installations-search.ts +19 -0
  210. package/src/routes/-releases-search.ts +74 -0
  211. package/src/routes/__root.tsx +174 -0
  212. package/src/routes/api/auth/$.ts +19 -0
  213. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  214. package/src/routes/api-keys.tsx +37 -0
  215. package/src/routes/index.tsx +982 -0
  216. package/src/routes/insights.tsx +139 -0
  217. package/src/routes/installations.tsx +194 -0
  218. package/src/routes/signing.tsx +37 -0
  219. package/src/styles.css +152 -0
  220. package/src/virtual-modules.d.ts +21 -0
  221. package/src/vite-env.d.ts +1 -0
  222. package/src/vite-internal.ts +195 -0
  223. package/src/vite.ts +12 -0
  224. package/.output/public/assets/dist-DJX53faP.js +0 -3
  225. package/.output/public/assets/index-Iylis5kY.js +0 -10
  226. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  227. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  228. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  229. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  230. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  231. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  232. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  233. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  234. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  235. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  236. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  237. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  238. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  239. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  240. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  241. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  242. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  243. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  244. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  245. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  246. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  247. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  248. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  249. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  250. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  251. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  252. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  253. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  254. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  255. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  256. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  257. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  258. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  259. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  260. package/.output/server/node_modules/tslib/package.json +0 -47
  261. package/.output/server/node_modules/tslib/tslib.js +0 -484
  262. package/.output/server/package.json +0 -9
@@ -0,0 +1,377 @@
1
+ import {
2
+ AlertTriangle,
3
+ Check,
4
+ Clipboard,
5
+ KeyRound,
6
+ Loader2,
7
+ Plus,
8
+ RefreshCw,
9
+ ShieldOff,
10
+ } from "lucide-react";
11
+ import { type FormEvent, type MouseEvent, useState } from "react";
12
+ import { toast } from "sonner";
13
+
14
+ import {
15
+ Alert,
16
+ AlertAction,
17
+ AlertDescription,
18
+ AlertTitle,
19
+ } from "@/components/ui/alert";
20
+ import {
21
+ AlertDialog,
22
+ AlertDialogAction,
23
+ AlertDialogCancel,
24
+ AlertDialogContent,
25
+ AlertDialogDescription,
26
+ AlertDialogFooter,
27
+ AlertDialogHeader,
28
+ AlertDialogMedia,
29
+ AlertDialogTitle,
30
+ AlertDialogTrigger,
31
+ } from "@/components/ui/alert-dialog";
32
+ import { Badge } from "@/components/ui/badge";
33
+ import { Button } from "@/components/ui/button";
34
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
35
+ import {
36
+ Dialog,
37
+ DialogContent,
38
+ DialogDescription,
39
+ DialogFooter,
40
+ DialogHeader,
41
+ DialogTitle,
42
+ } from "@/components/ui/dialog";
43
+ import {
44
+ Field,
45
+ FieldDescription,
46
+ FieldError,
47
+ FieldGroup,
48
+ FieldLabel,
49
+ } from "@/components/ui/field";
50
+ import { Input } from "@/components/ui/input";
51
+ import { Skeleton } from "@/components/ui/skeleton";
52
+ import {
53
+ Table,
54
+ TableBody,
55
+ TableCell,
56
+ TableHead,
57
+ TableHeader,
58
+ TableRow,
59
+ } from "@/components/ui/table";
60
+ import {
61
+ useApiKeysQuery,
62
+ useCreateApiKeyMutation,
63
+ useRevokeApiKeyMutation,
64
+ } from "@/lib/api-keys-api";
65
+ import type { ApiKeyView } from "@/lib/api-keys-rpc";
66
+
67
+ const formatCreatedAt = (createdAt: number): string =>
68
+ new Date(createdAt).toISOString().slice(0, 10);
69
+
70
+ function CreateApiKeyDialog() {
71
+ const [open, setOpen] = useState(false);
72
+ const [name, setName] = useState("");
73
+ const [apiKey, setApiKey] = useState<string | null>(null);
74
+ const create = useCreateApiKeyMutation();
75
+ const normalizedName = name.trim();
76
+ const nameError =
77
+ normalizedName.length > 64 ? "Name must be 64 characters or fewer." : null;
78
+
79
+ const reset = () => {
80
+ setName("");
81
+ setApiKey(null);
82
+ create.reset();
83
+ };
84
+
85
+ const handleOpenChange = (nextOpen: boolean) => {
86
+ setOpen(nextOpen);
87
+ if (!nextOpen) reset();
88
+ };
89
+
90
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
91
+ event.preventDefault();
92
+ if (normalizedName.length === 0 || nameError !== null) return;
93
+ try {
94
+ const result = await create.mutateAsync(normalizedName);
95
+ setApiKey(result.apiKey);
96
+ toast.success("API key created");
97
+ } catch (error) {
98
+ toast.error(
99
+ error instanceof Error ? error.message : "Unable to create API key",
100
+ );
101
+ }
102
+ };
103
+
104
+ const copyKey = async () => {
105
+ if (apiKey === null) return;
106
+ try {
107
+ await navigator.clipboard.writeText(apiKey);
108
+ toast.success("API key copied");
109
+ } catch {
110
+ toast.error("Unable to copy API key");
111
+ }
112
+ };
113
+
114
+ return (
115
+ <>
116
+ <Button onClick={() => setOpen(true)}>
117
+ <Plus data-icon="inline-start" />
118
+ Create API key
119
+ </Button>
120
+ <Dialog open={open} onOpenChange={handleOpenChange}>
121
+ <DialogContent showCloseButton={apiKey === null}>
122
+ <DialogHeader>
123
+ <DialogTitle>
124
+ {apiKey === null ? "Create API key" : "Save your API key"}
125
+ </DialogTitle>
126
+ <DialogDescription>
127
+ {apiKey === null
128
+ ? "Name the app or environment that will use this key."
129
+ : "This key is shown once. Store it securely before closing."}
130
+ </DialogDescription>
131
+ </DialogHeader>
132
+ {apiKey === null ? (
133
+ <form onSubmit={handleSubmit}>
134
+ <FieldGroup>
135
+ <Field data-invalid={nameError !== null || undefined}>
136
+ <FieldLabel htmlFor="api-key-name">Name</FieldLabel>
137
+ <Input
138
+ id="api-key-name"
139
+ autoComplete="off"
140
+ maxLength={65}
141
+ onChange={(event) => setName(event.target.value)}
142
+ placeholder="Production app"
143
+ value={name}
144
+ />
145
+ <FieldDescription>
146
+ Use a name you can identify before revoking the key.
147
+ </FieldDescription>
148
+ <FieldError>{nameError}</FieldError>
149
+ </Field>
150
+ </FieldGroup>
151
+ <DialogFooter className="mt-4">
152
+ <Button
153
+ disabled={
154
+ create.isPending ||
155
+ normalizedName.length === 0 ||
156
+ nameError !== null
157
+ }
158
+ type="submit"
159
+ >
160
+ {create.isPending ? (
161
+ <Loader2
162
+ data-icon="inline-start"
163
+ className="animate-spin"
164
+ />
165
+ ) : (
166
+ <KeyRound data-icon="inline-start" />
167
+ )}
168
+ Create API key
169
+ </Button>
170
+ </DialogFooter>
171
+ </form>
172
+ ) : (
173
+ <FieldGroup>
174
+ <Field>
175
+ <FieldLabel htmlFor="created-api-key">API key</FieldLabel>
176
+ <div className="flex gap-2">
177
+ <Input
178
+ id="created-api-key"
179
+ className="font-mono"
180
+ readOnly
181
+ value={apiKey}
182
+ />
183
+ <Button onClick={copyKey} type="button" variant="outline">
184
+ <Clipboard data-icon="inline-start" />
185
+ Copy
186
+ </Button>
187
+ </div>
188
+ </Field>
189
+ <DialogFooter>
190
+ <Button onClick={() => handleOpenChange(false)}>
191
+ <Check data-icon="inline-start" />
192
+ Done
193
+ </Button>
194
+ </DialogFooter>
195
+ </FieldGroup>
196
+ )}
197
+ </DialogContent>
198
+ </Dialog>
199
+ </>
200
+ );
201
+ }
202
+
203
+ function RevokeApiKeyDialog({ record }: { record: ApiKeyView }) {
204
+ const [open, setOpen] = useState(false);
205
+ const revoke = useRevokeApiKeyMutation();
206
+
207
+ const handleRevoke = async (event: MouseEvent<HTMLButtonElement>) => {
208
+ event.preventDefault();
209
+ try {
210
+ await revoke.mutateAsync(record.id);
211
+ setOpen(false);
212
+ toast.success("API key revoked");
213
+ } catch (error) {
214
+ toast.error(
215
+ error instanceof Error ? error.message : "Unable to revoke API key",
216
+ );
217
+ }
218
+ };
219
+
220
+ return (
221
+ <AlertDialog
222
+ open={open}
223
+ onOpenChange={(nextOpen) => {
224
+ if (!revoke.isPending) setOpen(nextOpen);
225
+ }}
226
+ >
227
+ <AlertDialogTrigger render={<Button size="sm" variant="ghost" />}>
228
+ <ShieldOff data-icon="inline-start" />
229
+ Revoke
230
+ </AlertDialogTrigger>
231
+ <AlertDialogContent>
232
+ <AlertDialogHeader>
233
+ <AlertDialogMedia>
234
+ <ShieldOff />
235
+ </AlertDialogMedia>
236
+ <AlertDialogTitle>Revoke {record.name}?</AlertDialogTitle>
237
+ <AlertDialogDescription>
238
+ Apps using {record.prefix}… will immediately lose OTA update and
239
+ insights access. This action cannot be undone.
240
+ </AlertDialogDescription>
241
+ </AlertDialogHeader>
242
+ <AlertDialogFooter>
243
+ <AlertDialogCancel disabled={revoke.isPending}>
244
+ Cancel
245
+ </AlertDialogCancel>
246
+ <AlertDialogAction
247
+ disabled={revoke.isPending}
248
+ onClick={(event) => void handleRevoke(event)}
249
+ variant="destructive"
250
+ >
251
+ Revoke API key
252
+ </AlertDialogAction>
253
+ </AlertDialogFooter>
254
+ </AlertDialogContent>
255
+ </AlertDialog>
256
+ );
257
+ }
258
+
259
+ export function ApiKeysPage() {
260
+ const apiKeys = useApiKeysQuery();
261
+
262
+ return (
263
+ <Card className="overflow-hidden shadow-sm">
264
+ <CardHeader className="flex-row items-center justify-between space-y-0 border-b px-4 py-3">
265
+ <CardTitle className="text-sm">
266
+ <h2>API keys</h2>
267
+ </CardTitle>
268
+ <CreateApiKeyDialog />
269
+ </CardHeader>
270
+ <CardContent className="p-0">
271
+ {apiKeys.isError ? (
272
+ <div className="p-4">
273
+ <Alert variant="destructive">
274
+ <AlertTriangle />
275
+ <AlertTitle>API keys couldn't be loaded</AlertTitle>
276
+ <AlertDescription>
277
+ Check your connection and try again.
278
+ </AlertDescription>
279
+ <AlertAction>
280
+ <Button
281
+ onClick={() => void apiKeys.refetch()}
282
+ size="xs"
283
+ variant="outline"
284
+ >
285
+ <RefreshCw data-icon="inline-start" />
286
+ Retry
287
+ </Button>
288
+ </AlertAction>
289
+ </Alert>
290
+ </div>
291
+ ) : (
292
+ <Table aria-label="API keys" className="table-fixed">
293
+ <TableHeader>
294
+ <TableRow>
295
+ <TableHead>Key</TableHead>
296
+ <TableHead className="w-32">Created</TableHead>
297
+ <TableHead className="w-24 text-right">
298
+ <span className="sr-only">Actions</span>
299
+ </TableHead>
300
+ </TableRow>
301
+ </TableHeader>
302
+ <TableBody>
303
+ {apiKeys.isLoading ? (
304
+ Array.from({ length: 3 }, (_, index) => (
305
+ <TableRow key={index}>
306
+ <TableCell className="space-y-2 py-3">
307
+ {index === 0 ? (
308
+ <span className="sr-only">Loading API keys</span>
309
+ ) : null}
310
+ <Skeleton className="h-3.5 w-32" />
311
+ <Skeleton className="h-3 w-20" />
312
+ </TableCell>
313
+ <TableCell>
314
+ <Skeleton className="h-3.5 w-20" />
315
+ </TableCell>
316
+ <TableCell>
317
+ <Skeleton className="ml-auto h-6 w-16" />
318
+ </TableCell>
319
+ </TableRow>
320
+ ))
321
+ ) : apiKeys.data?.length === 0 ? (
322
+ <TableRow>
323
+ <TableCell
324
+ colSpan={3}
325
+ className="h-40 text-center text-muted-foreground"
326
+ >
327
+ <div className="mx-auto flex max-w-xs flex-col items-center gap-2">
328
+ <KeyRound className="size-5" />
329
+ <p className="font-medium text-foreground">No API keys</p>
330
+ <p>Create an API key to connect an app.</p>
331
+ </div>
332
+ </TableCell>
333
+ </TableRow>
334
+ ) : (
335
+ apiKeys.data?.map((apiKey) => (
336
+ <TableRow
337
+ className={
338
+ apiKey.revoked_at_ms === null
339
+ ? undefined
340
+ : "text-muted-foreground"
341
+ }
342
+ key={apiKey.id}
343
+ >
344
+ <TableCell className="whitespace-normal py-3">
345
+ <div className="min-w-0 space-y-1">
346
+ <p className="break-words font-medium text-foreground">
347
+ {apiKey.name}
348
+ </p>
349
+ <p className="font-mono text-xs text-muted-foreground">
350
+ {apiKey.prefix}…
351
+ </p>
352
+ </div>
353
+ </TableCell>
354
+ <TableCell className="text-xs text-muted-foreground">
355
+ <time
356
+ dateTime={new Date(apiKey.created_at_ms).toISOString()}
357
+ >
358
+ {formatCreatedAt(apiKey.created_at_ms)}
359
+ </time>
360
+ </TableCell>
361
+ <TableCell className="text-right">
362
+ {apiKey.revoked_at_ms === null ? (
363
+ <RevokeApiKeyDialog record={apiKey} />
364
+ ) : (
365
+ <Badge variant="outline">Revoked</Badge>
366
+ )}
367
+ </TableCell>
368
+ </TableRow>
369
+ ))
370
+ )}
371
+ </TableBody>
372
+ </Table>
373
+ )}
374
+ </CardContent>
375
+ </Card>
376
+ );
377
+ }
@@ -0,0 +1,193 @@
1
+ import type { ActiveInstallationWindow } from "@hot-updater/server";
2
+ import { useId } from "react";
3
+ import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
4
+
5
+ import {
6
+ type ChartConfig,
7
+ ChartContainer,
8
+ ChartTooltip,
9
+ ChartTooltipContent,
10
+ } from "@/components/ui/chart";
11
+
12
+ interface SeriesPoint {
13
+ readonly bucketStartMs: number;
14
+ readonly value: number;
15
+ }
16
+
17
+ interface BundleActivityChartProps {
18
+ readonly installed: readonly SeriesPoint[];
19
+ readonly recovered: readonly SeriesPoint[];
20
+ readonly window: ActiveInstallationWindow;
21
+ }
22
+
23
+ interface ActivityChartPoint {
24
+ readonly bucketStartMs: number;
25
+ readonly installed: number;
26
+ readonly recovered: number;
27
+ }
28
+
29
+ const chartConfig = {
30
+ installed: {
31
+ label: "Applied",
32
+ color: "var(--chart-2)",
33
+ },
34
+ recovered: {
35
+ label: "Recovered",
36
+ color: "var(--muted-foreground)",
37
+ },
38
+ } satisfies ChartConfig;
39
+
40
+ const dateFormatter = new Intl.DateTimeFormat("en", {
41
+ day: "numeric",
42
+ month: "short",
43
+ timeZone: "UTC",
44
+ });
45
+ const hourFormatter = new Intl.DateTimeFormat("en", {
46
+ day: "numeric",
47
+ hour: "numeric",
48
+ month: "short",
49
+ timeZone: "UTC",
50
+ });
51
+
52
+ const windowLabels = {
53
+ "24h": "24 hours",
54
+ "7d": "7 days",
55
+ "30d": "30 days",
56
+ } as const;
57
+
58
+ const mergeActivitySeries = (
59
+ installed: readonly SeriesPoint[],
60
+ recovered: readonly SeriesPoint[],
61
+ ): readonly ActivityChartPoint[] => {
62
+ const installedByBucket = new Map(
63
+ installed.map((point) => [point.bucketStartMs, point.value] as const),
64
+ );
65
+ const recoveredByBucket = new Map(
66
+ recovered.map((point) => [point.bucketStartMs, point.value] as const),
67
+ );
68
+ const buckets = new Set([
69
+ ...installedByBucket.keys(),
70
+ ...recoveredByBucket.keys(),
71
+ ]);
72
+
73
+ return [...buckets]
74
+ .sort((left, right) => left - right)
75
+ .map((bucketStartMs) => ({
76
+ bucketStartMs,
77
+ installed: installedByBucket.get(bucketStartMs) ?? 0,
78
+ recovered: recoveredByBucket.get(bucketStartMs) ?? 0,
79
+ }));
80
+ };
81
+
82
+ const formatBucket = (
83
+ value: number,
84
+ window: ActiveInstallationWindow,
85
+ ): string =>
86
+ (window === "24h" ? hourFormatter : dateFormatter).format(new Date(value));
87
+
88
+ export function BundleActivityChart({
89
+ installed,
90
+ recovered,
91
+ window,
92
+ }: BundleActivityChartProps) {
93
+ const captionId = useId();
94
+ const chartData = mergeActivitySeries(installed, recovered);
95
+ const hasWindowActivity = chartData.some(
96
+ (point) => point.installed > 0 || point.recovered > 0,
97
+ );
98
+
99
+ if (!hasWindowActivity) {
100
+ return (
101
+ <div className="flex h-16 items-center justify-center border-t px-4 pt-3 text-center text-xs text-muted-foreground sm:h-20 sm:pt-4">
102
+ No device activity in the last 30 days.
103
+ </div>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <div className="border-t pt-3 sm:pt-4">
109
+ <ChartContainer
110
+ aria-describedby={captionId}
111
+ aria-label={`Bundle movement over ${windowLabels[window]}`}
112
+ className="h-24 w-full aspect-auto sm:h-32"
113
+ config={chartConfig}
114
+ role="img"
115
+ >
116
+ <AreaChart
117
+ accessibilityLayer
118
+ data={chartData}
119
+ margin={{ left: -16, right: 4, top: 4 }}
120
+ >
121
+ <CartesianGrid vertical={false} />
122
+ <XAxis
123
+ axisLine={false}
124
+ dataKey="bucketStartMs"
125
+ minTickGap={28}
126
+ tickFormatter={(value: number) => formatBucket(value, window)}
127
+ tickLine={false}
128
+ />
129
+ <YAxis
130
+ allowDecimals={false}
131
+ axisLine={false}
132
+ tickLine={false}
133
+ width={32}
134
+ />
135
+ <ChartTooltip
136
+ content={
137
+ <ChartTooltipContent
138
+ labelFormatter={(_, payload) => {
139
+ const bucket = payload[0]?.payload.bucketStartMs;
140
+ return typeof bucket === "number"
141
+ ? formatBucket(bucket, window)
142
+ : "";
143
+ }}
144
+ />
145
+ }
146
+ />
147
+ <Area
148
+ dataKey="installed"
149
+ fill="var(--color-installed)"
150
+ fillOpacity={0.12}
151
+ isAnimationActive={false}
152
+ stroke="var(--color-installed)"
153
+ strokeWidth={2}
154
+ type="monotone"
155
+ />
156
+ <Area
157
+ dataKey="recovered"
158
+ fill="var(--color-recovered)"
159
+ fillOpacity={0.04}
160
+ isAnimationActive={false}
161
+ stroke="var(--color-recovered)"
162
+ strokeWidth={1.5}
163
+ type="monotone"
164
+ />
165
+ </AreaChart>
166
+ </ChartContainer>
167
+ <div className="sr-only">
168
+ <table>
169
+ <caption id={captionId}>
170
+ Distinct bundle movement in each bucket over {windowLabels[window]}.
171
+ Times are shown in UTC.
172
+ </caption>
173
+ <thead>
174
+ <tr>
175
+ <th scope="col">Date</th>
176
+ <th scope="col">Applied</th>
177
+ <th scope="col">Recovered</th>
178
+ </tr>
179
+ </thead>
180
+ <tbody>
181
+ {chartData.map((point) => (
182
+ <tr key={point.bucketStartMs}>
183
+ <th scope="row">{formatBucket(point.bucketStartMs, window)}</th>
184
+ <td>{point.installed}</td>
185
+ <td>{point.recovered}</td>
186
+ </tr>
187
+ ))}
188
+ </tbody>
189
+ </table>
190
+ </div>
191
+ </div>
192
+ );
193
+ }