@hot-updater/console 1.0.0-rc.0 → 1.0.0-rc.2

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 (203) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +1 -0
  3. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +1 -0
  4. package/.output/public/assets/api-keys-BzKocqRy.js +1 -0
  5. package/.output/public/assets/api-keys-api-BHcODB7q.js +1 -0
  6. package/.output/public/assets/card-Dhr_eFYj.js +1 -0
  7. package/.output/public/assets/{chart-no-axes-combined-FaYMGmQS.js → chart-no-axes-combined-Dy4WJv2U.js} +1 -1
  8. package/.output/public/assets/composite-Bp98A0Ld.js +1 -0
  9. package/.output/public/assets/createServerFn-CA8fs7QD.js +3 -0
  10. package/.output/public/assets/dialog-CUTrcPlB.js +1 -0
  11. package/.output/public/assets/{dist-DspNKfMj.js → dist-C_NydL0e.js} +1 -1
  12. package/.output/public/assets/index-DdAz-CXv.js +10 -0
  13. package/.output/public/assets/input-group-cSOG4wh_.js +1 -0
  14. package/.output/public/assets/insights-C0PBSZ-n.js +1 -0
  15. package/.output/public/assets/installations-CE33wgbY.js +2 -0
  16. package/.output/public/assets/installations-CStZja8x.js +1 -0
  17. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +1 -0
  18. package/.output/public/assets/link-CI8idAUi.js +1 -0
  19. package/.output/public/assets/loader-circle-BoxLrsOu.js +1 -0
  20. package/.output/public/assets/refresh-cw-Bqc72UZG.js +1 -0
  21. package/.output/public/assets/{routes-DcQnVajO.js → routes-DGVXdzpw.js} +2 -2
  22. package/.output/public/assets/routes-DTlwOr8I.js +1 -0
  23. package/.output/public/assets/{shield-check-DE_hVuHu.js → shield-check-uK3MHWJZ.js} +1 -1
  24. package/.output/public/assets/signing-CMf4aqDO.js +1 -0
  25. package/.output/public/assets/styles-BCvbtKsm.css +2 -0
  26. package/.output/public/assets/table-2vcV89QB.js +1 -0
  27. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +1 -0
  28. package/.output/public/assets/x-DCd0j8c6.js +1 -0
  29. package/.output/server/{__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +44 -52
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +1906 -4040
  31. package/.output/server/_libs/@tanstack/react-router+[...].mjs +52 -4
  32. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
  33. package/.output/server/_libs/lucide-react.mjs +36 -3
  34. package/.output/server/_libs/next-themes.mjs +2 -2
  35. package/.output/server/_libs/sonner.mjs +2 -2
  36. package/.output/server/_libs/tanstack__react-query.mjs +2 -2
  37. package/.output/server/_libs/verkit.mjs +20 -15
  38. package/.output/server/_runtime.mjs +1 -10
  39. package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +55 -0
  40. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +484 -0
  41. package/.output/server/_ssr/{api-keys-BnM_o2nS.mjs → api-keys-CZeTPQys.mjs} +11 -13
  42. package/.output/server/_ssr/{api-keys-api-CIywsSF5.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BSp1wkIV.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-cG5wd_tO.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BrJ6obHJ.mjs → auth-rpc-DUaeOWKK.mjs} +4 -4
  46. package/.output/server/_ssr/{auth.server-DwKoZoQM.mjs → auth.server-DW71WzsD.mjs} +2 -5
  47. package/.output/server/_ssr/{bundle-signing-rpc-COoQhW8E.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  48. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  49. package/.output/server/_ssr/{alert-Dim3MGY5.mjs → card-BRiPmi0C.mjs} +61 -39
  50. package/.output/server/_ssr/{config.server-DEiLhKmy.mjs → config.server-afDuAwaG.mjs} +15 -8
  51. package/.output/server/_ssr/{createSsrRpc-9P0S0lJ3.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  52. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  53. package/.output/server/_ssr/{databaseRows-CwbNwYSl.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  54. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  55. package/.output/server/_ssr/{dialog-CQuRMNZ4.mjs → dialog-DS-nw-pA.mjs} +2 -2
  56. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  57. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  58. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  59. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  60. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  61. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  62. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  63. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  64. package/.output/server/_ssr/{router-C-KUBfOi.mjs → router-B9wG_9f8.mjs} +37 -46
  65. package/.output/server/_ssr/{routes-Z7TwC6nC.mjs → routes-CKrrS2q2.mjs} +3 -3
  66. package/.output/server/_ssr/{routes-BM4ht9jb.mjs → routes-D7Yh_jfj.mjs} +402 -390
  67. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  68. package/.output/server/_ssr/{server-BjzmvXnO.mjs → server-D_C2rMzf.mjs} +5 -5
  69. package/.output/server/_ssr/{signing-k8GxyBDF.mjs → signing-Dz1WkZOf.mjs} +16 -85
  70. package/.output/server/_ssr/ssr.mjs +1 -1
  71. package/.output/server/_ssr/{field-DRIXKjZG.mjs → table-CnZmon_Z.mjs} +51 -5
  72. package/.output/server/_ssr/{uuidv7-BbJ-9D_I.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  73. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  74. package/.output/server/index.mjs +168 -203
  75. package/lib/index.d.mts +4 -4
  76. package/lib/vite-internal.mjs +1 -1
  77. package/lib/vite.d.mts +0 -1
  78. package/package.json +8 -8
  79. package/src/components/AppSidebar.tsx +12 -16
  80. package/src/components/ConsoleAccessPage.tsx +1 -1
  81. package/src/components/HashValueDisplay.tsx +11 -2
  82. package/src/components/features/api-keys/ApiKeysPage.tsx +1 -1
  83. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  84. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  85. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  86. package/src/components/features/insights/EventDetails.tsx +151 -0
  87. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  88. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  89. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  90. package/src/components/features/insights/InsightsControls.tsx +121 -0
  91. package/src/components/features/{analytics/AnalyticsErrorAlert.tsx → insights/InsightsErrorAlert.tsx} +6 -6
  92. package/src/components/features/insights/InsightsOverview.tsx +203 -0
  93. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  94. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  95. package/src/components/features/insights/InstallationHistoryCard.tsx +250 -0
  96. package/src/components/features/insights/InstallationMatchesCard.tsx +178 -0
  97. package/src/components/features/insights/InstallationPageHeader.tsx +90 -0
  98. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  99. package/src/components/features/signing/BundleSigningPage.tsx +18 -126
  100. package/src/components/ui/badge.tsx +2 -0
  101. package/src/components/ui/combobox.tsx +1 -0
  102. package/src/components/ui/skeleton.tsx +4 -1
  103. package/src/components/ui/tooltip.tsx +1 -1
  104. package/src/index.ts +7 -3
  105. package/src/lib/api-rpc.ts +19 -90
  106. package/src/lib/api.ts +0 -99
  107. package/src/lib/insights-api.ts +100 -0
  108. package/src/lib/insights-rpc.ts +88 -0
  109. package/src/lib/insights-view.ts +15 -0
  110. package/src/lib/public-bundle-error.ts +19 -0
  111. package/src/lib/server/bundle-signing.server.ts +5 -82
  112. package/src/lib/server/console-runtime.server.ts +0 -9
  113. package/src/lib/server/releaseReachability.ts +1 -4
  114. package/src/lib/server/runtime.server.ts +6 -142
  115. package/src/routeTree.gen.ts +21 -21
  116. package/src/router.tsx +3 -0
  117. package/src/routes/-installations-search.ts +25 -12
  118. package/src/routes/__root.tsx +6 -16
  119. package/src/routes/api-keys.tsx +1 -1
  120. package/src/routes/index.tsx +21 -81
  121. package/src/routes/insights.tsx +84 -0
  122. package/src/routes/installations.tsx +294 -142
  123. package/src/styles.css +6 -0
  124. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +0 -1
  125. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +0 -42
  126. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +0 -1
  127. package/.output/public/assets/alert-C-1KvDW8.js +0 -1
  128. package/.output/public/assets/analytics-Bqz0c-BT.js +0 -1
  129. package/.output/public/assets/analytics-api-CJdrTLLO.js +0 -1
  130. package/.output/public/assets/api-B_Rh-uzC.js +0 -1
  131. package/.output/public/assets/api-keys-BurCFhzH.js +0 -1
  132. package/.output/public/assets/api-keys-api-BEDJdQg3.js +0 -1
  133. package/.output/public/assets/badge-Bf0fZ-Gx.js +0 -1
  134. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  135. package/.output/public/assets/dialog-BS60BRr2.js +0 -1
  136. package/.output/public/assets/download-O-yNBc9H.js +0 -1
  137. package/.output/public/assets/field-Buq148-A.js +0 -1
  138. package/.output/public/assets/index-kFhd2S78.js +0 -10
  139. package/.output/public/assets/installations-BrQ-od41.js +0 -2
  140. package/.output/public/assets/installations-CGlZ3ROz.js +0 -1
  141. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +0 -1
  142. package/.output/public/assets/loader-circle-DCYF-wJl.js +0 -1
  143. package/.output/public/assets/refresh-cw-n8XYbPPj.js +0 -1
  144. package/.output/public/assets/routes-CrdokJ80.js +0 -1
  145. package/.output/public/assets/search-DLhbWw3W.js +0 -1
  146. package/.output/public/assets/signing-HRexfjsH.js +0 -1
  147. package/.output/public/assets/styles-eB4EtvFS.css +0 -2
  148. package/.output/public/assets/table-D8CLieHX.js +0 -1
  149. package/.output/public/assets/textarea-YJLTVAoq.js +0 -1
  150. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  151. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +0 -1
  152. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  153. package/.output/server/_libs/d3-array.mjs +0 -231
  154. package/.output/server/_libs/d3-color.mjs +0 -330
  155. package/.output/server/_libs/d3-format.mjs +0 -252
  156. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  157. package/.output/server/_libs/d3-path.mjs +0 -77
  158. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  159. package/.output/server/_libs/d3-shape.mjs +0 -818
  160. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  161. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  162. package/.output/server/_libs/eventemitter3.mjs +0 -236
  163. package/.output/server/_libs/immer.mjs +0 -532
  164. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  165. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  166. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +0 -15
  167. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +0 -288
  168. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +0 -13
  169. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +0 -858
  170. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +0 -71
  171. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +0 -96
  172. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +0 -155
  173. package/.output/server/_ssr/api-BTSv1tZb.mjs +0 -282
  174. package/.output/server/_ssr/badge-DCxbEeof.mjs +0 -26
  175. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +0 -51
  176. package/.output/server/_ssr/installations-B-azgAvX.mjs +0 -19
  177. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +0 -535
  178. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +0 -527
  179. package/.output/server/_ssr/table-v1yCvYo7.mjs +0 -53
  180. package/.output/server/_ssr/textarea-DYHnskhb.mjs +0 -14
  181. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +0 -108
  182. package/src/components/features/analytics/ActivityChart.tsx +0 -150
  183. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +0 -24
  184. package/src/components/features/analytics/AnalyticsControls.tsx +0 -115
  185. package/src/components/features/analytics/AnalyticsOverview.tsx +0 -208
  186. package/src/components/features/analytics/BundleDistribution.tsx +0 -133
  187. package/src/components/features/analytics/BundleSelector.tsx +0 -114
  188. package/src/components/features/analytics/InstallationHistoryCard.tsx +0 -248
  189. package/src/components/features/analytics/InstallationMatchesCard.tsx +0 -132
  190. package/src/components/features/analytics/InstallationPageHeader.tsx +0 -138
  191. package/src/components/features/analytics/InstallationPagination.tsx +0 -48
  192. package/src/components/features/analytics/InstallationSearch.tsx +0 -230
  193. package/src/components/features/analytics/UpdateOutcomes.tsx +0 -147
  194. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  195. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +0 -123
  196. package/src/lib/analytics-api.ts +0 -145
  197. package/src/lib/analytics-error.ts +0 -19
  198. package/src/lib/analytics-input.ts +0 -187
  199. package/src/lib/analytics-overview.ts +0 -170
  200. package/src/lib/analytics-rpc.ts +0 -218
  201. package/src/lib/server/releaseActivity.ts +0 -27
  202. package/src/routes/analytics.tsx +0 -139
  203. /package/.output/public/assets/{preload-helper-TKOBYGYD.js → preload-helper-CQ36nxok.js} +0 -0
@@ -1,138 +0,0 @@
1
- import { Link } from "@tanstack/react-router";
2
- import { ArrowLeft, Search, X } from "lucide-react";
3
-
4
- import { Button, buttonVariants } from "@/components/ui/button";
5
- import {
6
- Card,
7
- CardContent,
8
- CardDescription,
9
- CardTitle,
10
- } from "@/components/ui/card";
11
- import {
12
- Field,
13
- FieldDescription,
14
- FieldGroup,
15
- FieldLabel,
16
- } from "@/components/ui/field";
17
- import {
18
- InputGroup,
19
- InputGroupAddon,
20
- InputGroupButton,
21
- InputGroupInput,
22
- } from "@/components/ui/input-group";
23
- import { Separator } from "@/components/ui/separator";
24
- import { SidebarTrigger } from "@/components/ui/sidebar";
25
- import { Skeleton } from "@/components/ui/skeleton";
26
-
27
- export function InstallationPageHeader() {
28
- return (
29
- <header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-x-2 gap-y-1 border-b bg-background px-3 py-3 sm:min-h-12 sm:flex-nowrap sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
30
- <SidebarTrigger className="-ml-1" />
31
- <Link
32
- className={buttonVariants({ size: "sm", variant: "ghost" })}
33
- to="/analytics"
34
- >
35
- <ArrowLeft aria-hidden="true" data-icon="inline-start" />
36
- Back to Analytics
37
- </Link>
38
- <Separator className="mx-1 hidden h-4 sm:block" orientation="vertical" />
39
- <div className="basis-full pl-9 sm:basis-auto sm:pl-0">
40
- <h1 className="text-sm font-medium">Installation history</h1>
41
- <p className="text-xs text-muted-foreground">
42
- Review the last known bundle and recorded changes.
43
- </p>
44
- </div>
45
- </header>
46
- );
47
- }
48
-
49
- export function InstallationSearchPanel({
50
- draftQuery,
51
- onDraftQueryChange,
52
- onSubmit,
53
- onClear,
54
- hasQuery,
55
- }: {
56
- readonly draftQuery: string;
57
- readonly onDraftQueryChange: (value: string) => void;
58
- readonly onSubmit: () => void;
59
- readonly onClear: () => void;
60
- readonly hasQuery: boolean;
61
- }) {
62
- return (
63
- <Card>
64
- <CardContent className="grid gap-4 p-4 lg:grid-cols-[minmax(12rem,0.45fr)_minmax(0,1.55fr)] lg:items-start">
65
- <div className="flex min-w-0 flex-col gap-1.5">
66
- <CardTitle className="text-sm font-medium">
67
- <h2>Find an installation</h2>
68
- </CardTitle>
69
- <CardDescription>
70
- Search by either identifier to inspect its bundle history.
71
- </CardDescription>
72
- </div>
73
- <form
74
- onSubmit={(event) => {
75
- event.preventDefault();
76
- onSubmit();
77
- }}
78
- role="search"
79
- >
80
- <FieldGroup>
81
- <Field>
82
- <FieldLabel htmlFor="installation-history-search">
83
- User ID or install ID
84
- </FieldLabel>
85
- <div className="flex flex-col gap-2 sm:flex-row">
86
- <InputGroup className="h-8">
87
- <InputGroupAddon>
88
- <Search aria-hidden="true" />
89
- </InputGroupAddon>
90
- <InputGroupInput
91
- id="installation-history-search"
92
- onChange={(event) => onDraftQueryChange(event.target.value)}
93
- placeholder="Enter a user ID or install ID"
94
- type="search"
95
- value={draftQuery}
96
- />
97
- {draftQuery.length > 0 || hasQuery ? (
98
- <InputGroupAddon align="inline-end">
99
- <InputGroupButton
100
- aria-label="Clear search"
101
- onClick={onClear}
102
- size="icon-xs"
103
- >
104
- <X aria-hidden="true" />
105
- </InputGroupButton>
106
- </InputGroupAddon>
107
- ) : null}
108
- </InputGroup>
109
- <Button
110
- className="w-full sm:w-auto"
111
- disabled={draftQuery.trim().length === 0}
112
- size="lg"
113
- type="submit"
114
- >
115
- <Search aria-hidden="true" data-icon="inline-start" />
116
- Search history
117
- </Button>
118
- </div>
119
- <FieldDescription>
120
- A user ID may return more than one installation. An install ID
121
- opens one installation directly.
122
- </FieldDescription>
123
- </Field>
124
- </FieldGroup>
125
- </form>
126
- </CardContent>
127
- </Card>
128
- );
129
- }
130
-
131
- export function InstallationResultsSkeleton() {
132
- return (
133
- <div className="grid items-stretch gap-6 lg:min-h-96 lg:grid-cols-[minmax(18rem,20rem)_minmax(0,1fr)]">
134
- <Skeleton className="h-64 w-full lg:h-full" />
135
- <Skeleton className="h-64 w-full lg:h-full" />
136
- </div>
137
- );
138
- }
@@ -1,48 +0,0 @@
1
- import { Button } from "@/components/ui/button";
2
-
3
- export function InstallationPagination({
4
- label,
5
- limit,
6
- offset,
7
- pageLength,
8
- total,
9
- onOffsetChange,
10
- }: {
11
- readonly label: string;
12
- readonly limit: number;
13
- readonly offset: number;
14
- readonly pageLength: number;
15
- readonly total: number;
16
- readonly onOffsetChange: (offset: number) => void;
17
- }) {
18
- return (
19
- <nav
20
- aria-label={`${label} pagination`}
21
- className="flex items-center justify-between gap-4 border-t px-5 py-5"
22
- >
23
- <span className="text-xs text-muted-foreground">
24
- {offset + 1}–{Math.min(offset + pageLength, total)} of {total}
25
- </span>
26
- <div className="flex gap-2">
27
- <Button
28
- disabled={offset === 0}
29
- onClick={() => onOffsetChange(Math.max(0, offset - limit))}
30
- size="sm"
31
- type="button"
32
- variant="outline"
33
- >
34
- Previous
35
- </Button>
36
- <Button
37
- disabled={offset + pageLength >= total}
38
- onClick={() => onOffsetChange(offset + limit)}
39
- size="sm"
40
- type="button"
41
- variant="outline"
42
- >
43
- Next
44
- </Button>
45
- </div>
46
- </nav>
47
- );
48
- }
@@ -1,230 +0,0 @@
1
- import { Link } from "@tanstack/react-router";
2
- import { Search, TriangleAlert } from "lucide-react";
3
- import { useState } from "react";
4
-
5
- import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6
- import { Button } from "@/components/ui/button";
7
- import {
8
- Card,
9
- CardContent,
10
- CardDescription,
11
- CardHeader,
12
- CardTitle,
13
- } from "@/components/ui/card";
14
- import {
15
- InputGroup,
16
- InputGroupAddon,
17
- InputGroupButton,
18
- InputGroupInput,
19
- } from "@/components/ui/input-group";
20
- import { Skeleton } from "@/components/ui/skeleton";
21
- import {
22
- Table,
23
- TableBody,
24
- TableCell,
25
- TableHead,
26
- TableHeader,
27
- TableRow,
28
- } from "@/components/ui/table";
29
- import { isAnalyticsQueryEnabled } from "@/lib/analytics-api";
30
- import {
31
- type InstallationSearchRow,
32
- useInstallationSearchQuery,
33
- } from "@/lib/api";
34
-
35
- import { useAnalyticsCapability } from "./AnalyticsCapabilityContext";
36
-
37
- const SEARCH_LIMIT = 20;
38
-
39
- const identityLabel = (row: InstallationSearchRow): string =>
40
- row.userId ?? row.installId;
41
-
42
- const platformLabel = (platform: InstallationSearchRow["platform"]): string =>
43
- platform === "ios" ? "iOS" : "Android";
44
-
45
- export function InstallationSearch({
46
- initialQuery = "",
47
- }: {
48
- readonly initialQuery?: string;
49
- }) {
50
- const capability = useAnalyticsCapability();
51
- const normalizedInitialQuery = initialQuery.trim();
52
- const [draftQuery, setDraftQuery] = useState(normalizedInitialQuery);
53
- const [submittedQuery, setSubmittedQuery] = useState(normalizedInitialQuery);
54
- const [offset, setOffset] = useState(0);
55
- const queryEnabled =
56
- isAnalyticsQueryEnabled(capability) && submittedQuery.length > 0;
57
- const { data, error, isLoading } = useInstallationSearchQuery(
58
- { query: submittedQuery, limit: SEARCH_LIMIT, offset },
59
- queryEnabled,
60
- );
61
-
62
- return (
63
- <Card>
64
- <CardHeader>
65
- <CardTitle className="text-sm font-medium">
66
- <h2>Installation inspector</h2>
67
- </CardTitle>
68
- <CardDescription>
69
- Search by user ID or install ID to inspect its last known bundle.
70
- </CardDescription>
71
- </CardHeader>
72
- <CardContent className="flex flex-col gap-5">
73
- <form
74
- className="max-w-2xl"
75
- onSubmit={(event) => {
76
- event.preventDefault();
77
- setSubmittedQuery(draftQuery.trim());
78
- setOffset(0);
79
- }}
80
- role="search"
81
- >
82
- <label className="sr-only" htmlFor="analytics-installation-search">
83
- User ID or install ID
84
- </label>
85
- <InputGroup className="h-9">
86
- <InputGroupAddon>
87
- <Search aria-hidden="true" />
88
- </InputGroupAddon>
89
- <InputGroupInput
90
- id="analytics-installation-search"
91
- aria-label="User ID or install ID"
92
- disabled={isLoading}
93
- onChange={(event) => setDraftQuery(event.target.value)}
94
- placeholder="Enter a user ID or install ID"
95
- type="search"
96
- value={draftQuery}
97
- />
98
- <InputGroupAddon align="inline-end">
99
- <InputGroupButton
100
- disabled={isLoading}
101
- type="submit"
102
- variant="default"
103
- >
104
- Search
105
- </InputGroupButton>
106
- </InputGroupAddon>
107
- </InputGroup>
108
- </form>
109
-
110
- {!queryEnabled ? (
111
- <p className="text-sm text-muted-foreground">
112
- Enter a user ID or install ID to search received reports.
113
- </p>
114
- ) : isLoading ? (
115
- <div
116
- aria-label="Searching installations"
117
- className="flex flex-col gap-2"
118
- >
119
- <Skeleton className="h-10 w-full" />
120
- <Skeleton className="h-10 w-full" />
121
- </div>
122
- ) : error ? (
123
- <Alert variant="destructive">
124
- <TriangleAlert aria-hidden="true" />
125
- <AlertTitle>Installation search unavailable</AlertTitle>
126
- <AlertDescription>{error.message}</AlertDescription>
127
- </Alert>
128
- ) : data && data.data.length > 0 ? (
129
- <div className="flex flex-col gap-3">
130
- <Table>
131
- <TableHeader>
132
- <TableRow className="hover:bg-transparent">
133
- <TableHead>Installation</TableHead>
134
- <TableHead>Last known bundle</TableHead>
135
- <TableHead>Reported context</TableHead>
136
- <TableHead className="text-right">Details</TableHead>
137
- </TableRow>
138
- </TableHeader>
139
- <TableBody>
140
- {data.data.map((row) => (
141
- <InstallationResultRow
142
- key={row.installId}
143
- query={submittedQuery}
144
- row={row}
145
- />
146
- ))}
147
- </TableBody>
148
- </Table>
149
- <div className="flex items-center justify-between gap-3">
150
- <p className="text-xs text-muted-foreground">
151
- {offset + 1}–
152
- {Math.min(offset + data.data.length, data.pagination.total)} of{" "}
153
- {data.pagination.total}
154
- </p>
155
- <div className="flex gap-2">
156
- <Button
157
- disabled={offset === 0}
158
- onClick={() => setOffset(Math.max(0, offset - SEARCH_LIMIT))}
159
- size="sm"
160
- type="button"
161
- variant="outline"
162
- >
163
- Previous
164
- </Button>
165
- <Button
166
- disabled={offset + data.data.length >= data.pagination.total}
167
- onClick={() => setOffset(offset + SEARCH_LIMIT)}
168
- size="sm"
169
- type="button"
170
- variant="outline"
171
- >
172
- Next
173
- </Button>
174
- </div>
175
- </div>
176
- </div>
177
- ) : (
178
- <p className="text-sm text-muted-foreground">
179
- No installations matched that search.
180
- </p>
181
- )}
182
- </CardContent>
183
- </Card>
184
- );
185
- }
186
-
187
- function InstallationResultRow({
188
- query,
189
- row,
190
- }: {
191
- readonly query: string;
192
- readonly row: InstallationSearchRow;
193
- }) {
194
- return (
195
- <TableRow>
196
- <TableCell className="max-w-64 whitespace-normal">
197
- <div className="flex flex-col gap-1">
198
- <span className="font-medium">{identityLabel(row)}</span>
199
- <code className="break-all text-muted-foreground">
200
- {row.installId}
201
- </code>
202
- </div>
203
- </TableCell>
204
- <TableCell className="max-w-64 whitespace-normal">
205
- <div className="flex flex-col gap-1">
206
- <span className="text-muted-foreground">Last known bundle</span>
207
- <code className="break-all">{row.lastKnownBundleId ?? "—"}</code>
208
- </div>
209
- </TableCell>
210
- <TableCell>
211
- {platformLabel(row.platform)} · {row.channel} · {row.appVersion}
212
- </TableCell>
213
- <TableCell className="text-right">
214
- <Link
215
- aria-label={`Open ${row.installId}`}
216
- className="font-medium text-primary underline-offset-4 hover:underline focus-visible:rounded-sm focus-visible:outline-2"
217
- search={{
218
- query,
219
- installId: row.installId,
220
- searchOffset: 0,
221
- historyOffset: 0,
222
- }}
223
- to="/installations"
224
- >
225
- Open
226
- </Link>
227
- </TableCell>
228
- </TableRow>
229
- );
230
- }
@@ -1,147 +0,0 @@
1
- import type { ActiveInstallationWindow } from "@hot-updater/server";
2
- import type { ReactNode } from "react";
3
-
4
- import {
5
- Card,
6
- CardContent,
7
- CardDescription,
8
- CardHeader,
9
- CardTitle,
10
- } from "@/components/ui/card";
11
- import { Skeleton } from "@/components/ui/skeleton";
12
- import type { BundleEventAnalytics } from "@/lib/api";
13
-
14
- import { BundleActivityChart } from "../bundles/BundleActivityChart";
15
- import { AnalyticsErrorAlert } from "./AnalyticsErrorAlert";
16
-
17
- export type UpdateOutcomeState =
18
- | { readonly status: "idle" }
19
- | { readonly status: "loading"; readonly bundleId: string }
20
- | {
21
- readonly status: "error";
22
- readonly bundleId: string;
23
- readonly error: Error;
24
- }
25
- | {
26
- readonly status: "success";
27
- readonly bundleId: string;
28
- readonly data: BundleEventAnalytics;
29
- };
30
-
31
- export function UpdateOutcomes({
32
- bundleSelector,
33
- configuredPercentage,
34
- latestBundleInstallations,
35
- reportingInstallations,
36
- state,
37
- window,
38
- }: {
39
- readonly bundleSelector: ReactNode;
40
- readonly configuredPercentage: number | null;
41
- readonly latestBundleInstallations: number;
42
- readonly reportingInstallations: number;
43
- readonly state: UpdateOutcomeState;
44
- readonly window: ActiveInstallationWindow;
45
- }) {
46
- const latestBundleShare =
47
- reportingInstallations === 0
48
- ? 0
49
- : (latestBundleInstallations / reportingInstallations) * 100;
50
-
51
- return (
52
- <Card className="min-w-0 overflow-hidden shadow-sm">
53
- <CardHeader className="gap-4 space-y-0 lg:flex-row lg:items-start lg:justify-between">
54
- <div className="flex min-w-0 flex-col gap-1.5">
55
- <CardTitle className="text-sm font-medium">
56
- <h2 id="bundle-detail-heading">Selected bundle activity</h2>
57
- </CardTitle>
58
- <CardDescription>
59
- Select a bundle to inspect its latest presence, applies, and
60
- recoveries during this period.
61
- </CardDescription>
62
- </div>
63
- {bundleSelector}
64
- </CardHeader>
65
- <CardContent className="pt-10">
66
- {state.status === "idle" ? (
67
- <p className="text-sm text-muted-foreground">
68
- No active bundle is available.
69
- </p>
70
- ) : state.status === "loading" ? (
71
- <div
72
- aria-label="Loading update outcomes"
73
- className="flex flex-col gap-4"
74
- >
75
- <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
76
- <Skeleton className="h-14 w-full" />
77
- <Skeleton className="h-14 w-full" />
78
- <Skeleton className="h-14 w-full" />
79
- <Skeleton className="h-14 w-full" />
80
- </div>
81
- <Skeleton className="h-32 w-full" />
82
- </div>
83
- ) : state.status === "error" ? (
84
- <AnalyticsErrorAlert
85
- error={state.error}
86
- fallbackTitle="Bundle movement unavailable"
87
- />
88
- ) : (
89
- <div className="flex min-w-0 flex-col gap-5">
90
- <dl className="grid sm:grid-cols-2 lg:grid-cols-4 lg:divide-x lg:divide-border/70">
91
- <div className="border-b pb-4 sm:pr-4 lg:border-b-0">
92
- <dt className="text-xs text-muted-foreground">
93
- Latest bundle share
94
- </dt>
95
- <dd className="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
96
- {Math.round(latestBundleShare)}%
97
- </dd>
98
- <dd className="mt-1 text-xs text-muted-foreground tabular-nums">
99
- {latestBundleInstallations.toLocaleString()} of{" "}
100
- {reportingInstallations.toLocaleString()} reporting installs
101
- </dd>
102
- </div>
103
- <div className="border-b py-4 sm:pl-4 lg:border-b-0 lg:py-0 lg:pr-4">
104
- <dt className="text-xs text-muted-foreground">Newly applied</dt>
105
- <dd className="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
106
- {state.data.summary.installed.toLocaleString()}
107
- </dd>
108
- <dd className="mt-1 text-xs text-muted-foreground">
109
- Distinct installations
110
- </dd>
111
- </div>
112
- <div className="border-b py-4 sm:pr-4 lg:border-b-0 lg:py-0 lg:pl-4">
113
- <dt className="text-xs text-muted-foreground">
114
- Recovered away
115
- </dt>
116
- <dd className="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
117
- {state.data.summary.recovered.toLocaleString()}
118
- </dd>
119
- <dd className="mt-1 text-xs text-muted-foreground">
120
- Distinct installations
121
- </dd>
122
- </div>
123
- <div className="pt-4 sm:pl-4 lg:py-0">
124
- <dt className="text-xs text-muted-foreground">
125
- Configured rollout
126
- </dt>
127
- <dd className="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
128
- {configuredPercentage === null
129
- ? "—"
130
- : `${configuredPercentage}%`}
131
- </dd>
132
- <dd className="mt-1 text-xs text-muted-foreground">
133
- Eligibility setting
134
- </dd>
135
- </div>
136
- </dl>
137
- <BundleActivityChart
138
- installed={state.data.series.installed}
139
- recovered={state.data.series.recovered}
140
- window={window}
141
- />
142
- </div>
143
- )}
144
- </CardContent>
145
- </Card>
146
- );
147
- }