@hot-updater/console 1.0.0-rc.1 → 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 (181) 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-C_NydL0e.js +1 -0
  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-BkbfZ62O.js → refresh-cw-Bqc72UZG.js} +1 -1
  21. package/.output/public/assets/{routes-BuoI5-Cr.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-Dftxlm2E.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +46 -54
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +283 -2417
  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-CpBqHpuC.mjs → api-keys-CZeTPQys.mjs} +9 -11
  42. package/.output/server/_ssr/{api-keys-api-Czkp57UV.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BTtMDSoR.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-BEX8ISai.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BFMEC-TT.mjs → auth-rpc-DUaeOWKK.mjs} +2 -2
  46. package/.output/server/_ssr/{bundle-signing-rpc-BbCFUQSu.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  47. package/.output/server/_ssr/{card-Bsc8mRis.mjs → card-BRiPmi0C.mjs} +25 -3
  48. package/.output/server/_ssr/{config.server-DQYEr_5X.mjs → config.server-afDuAwaG.mjs} +14 -7
  49. package/.output/server/_ssr/{createSsrRpc-DhfrxEjg.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  50. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  51. package/.output/server/_ssr/{databaseRows-QlCYLi0H.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  52. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  53. package/.output/server/_ssr/{dialog-C0vKUp8R.mjs → dialog-DS-nw-pA.mjs} +2 -2
  54. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  55. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  56. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  57. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  58. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  59. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  60. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  61. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  62. package/.output/server/_ssr/{router-BxBEvtF1.mjs → router-B9wG_9f8.mjs} +23 -32
  63. package/.output/server/_ssr/{routes-BC7rn-tf.mjs → routes-CKrrS2q2.mjs} +3 -3
  64. package/.output/server/_ssr/{routes-CM5KAwpB.mjs → routes-D7Yh_jfj.mjs} +402 -389
  65. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  66. package/.output/server/_ssr/{server-CpzW--lT.mjs → server-D_C2rMzf.mjs} +5 -5
  67. package/.output/server/_ssr/{signing-jk4HANnl.mjs → signing-Dz1WkZOf.mjs} +5 -6
  68. package/.output/server/_ssr/ssr.mjs +1 -1
  69. package/.output/server/_ssr/{field-DASCh-5d.mjs → table-CnZmon_Z.mjs} +51 -5
  70. package/.output/server/_ssr/{uuidv7-CaKuKXrV.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  71. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  72. package/.output/server/index.mjs +164 -178
  73. package/lib/index.d.mts +4 -3
  74. package/lib/vite-internal.mjs +1 -1
  75. package/lib/vite.d.mts +0 -1
  76. package/package.json +8 -8
  77. package/src/components/AppSidebar.tsx +10 -14
  78. package/src/components/ConsoleAccessPage.tsx +1 -1
  79. package/src/components/HashValueDisplay.tsx +11 -2
  80. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  81. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  82. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  83. package/src/components/features/insights/EventDetails.tsx +151 -0
  84. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  85. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  86. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  87. package/src/components/features/insights/InsightsControls.tsx +82 -76
  88. package/src/components/features/insights/InsightsErrorAlert.tsx +5 -5
  89. package/src/components/features/insights/InsightsOverview.tsx +156 -161
  90. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  91. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  92. package/src/components/features/insights/InstallationHistoryCard.tsx +178 -176
  93. package/src/components/features/insights/InstallationMatchesCard.tsx +111 -65
  94. package/src/components/features/insights/InstallationPageHeader.tsx +57 -105
  95. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  96. package/src/components/ui/badge.tsx +2 -0
  97. package/src/components/ui/combobox.tsx +1 -0
  98. package/src/components/ui/skeleton.tsx +4 -1
  99. package/src/components/ui/tooltip.tsx +1 -1
  100. package/src/index.ts +7 -2
  101. package/src/lib/api-rpc.ts +19 -90
  102. package/src/lib/api.ts +0 -99
  103. package/src/lib/insights-api.ts +93 -137
  104. package/src/lib/insights-rpc.ts +69 -199
  105. package/src/lib/insights-view.ts +15 -0
  106. package/src/lib/public-bundle-error.ts +19 -0
  107. package/src/lib/server/releaseReachability.ts +1 -4
  108. package/src/lib/server/runtime.server.ts +5 -141
  109. package/src/router.tsx +3 -0
  110. package/src/routes/-installations-search.ts +25 -12
  111. package/src/routes/__root.tsx +6 -16
  112. package/src/routes/index.tsx +21 -81
  113. package/src/routes/insights.tsx +50 -105
  114. package/src/routes/installations.tsx +290 -138
  115. package/src/styles.css +6 -0
  116. package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +0 -1
  117. package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +0 -1
  118. package/.output/public/assets/api-keys-api-DGVTTvTJ.js +0 -1
  119. package/.output/public/assets/api-keys-z16OZIP4.js +0 -1
  120. package/.output/public/assets/badge-Br_KDO0J.js +0 -1
  121. package/.output/public/assets/card-DzuEXvyJ.js +0 -1
  122. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  123. package/.output/public/assets/dialog-BeAENgez.js +0 -1
  124. package/.output/public/assets/dist-Bssh_cv0.js +0 -1
  125. package/.output/public/assets/field-Buq148-A.js +0 -1
  126. package/.output/public/assets/index-uOpQAIC4.js +0 -10
  127. package/.output/public/assets/input-group-B3p5UpyW.js +0 -1
  128. package/.output/public/assets/insights-DJTr7Ozn.js +0 -1
  129. package/.output/public/assets/insights-api-DVMOoRMS.js +0 -1
  130. package/.output/public/assets/installations-BY64iIAK.js +0 -2
  131. package/.output/public/assets/installations-D5v9i3AL.js +0 -1
  132. package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +0 -1
  133. package/.output/public/assets/routes-C8fvgETO.js +0 -1
  134. package/.output/public/assets/search-qsIDvS2I.js +0 -1
  135. package/.output/public/assets/signing-mJD88Y4C.js +0 -1
  136. package/.output/public/assets/styles-BZIG7t42.css +0 -2
  137. package/.output/public/assets/table-RamFEclP.js +0 -1
  138. package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +0 -42
  139. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  140. package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +0 -1
  141. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  142. package/.output/server/_libs/d3-array.mjs +0 -231
  143. package/.output/server/_libs/d3-color.mjs +0 -330
  144. package/.output/server/_libs/d3-format.mjs +0 -252
  145. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  146. package/.output/server/_libs/d3-path.mjs +0 -77
  147. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  148. package/.output/server/_libs/d3-shape.mjs +0 -818
  149. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  150. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  151. package/.output/server/_libs/eventemitter3.mjs +0 -236
  152. package/.output/server/_libs/immer.mjs +0 -532
  153. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  154. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  155. package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +0 -288
  156. package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +0 -13
  157. package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +0 -15
  158. package/.output/server/_ssr/badge-CpWXQwyF.mjs +0 -26
  159. package/.output/server/_ssr/input-group-etIsBUqv.mjs +0 -288
  160. package/.output/server/_ssr/insights-api-C1F7UreR.mjs +0 -71
  161. package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +0 -96
  162. package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +0 -155
  163. package/.output/server/_ssr/insights-ygT_aIfo.mjs +0 -858
  164. package/.output/server/_ssr/installations-DqRT1QTd.mjs +0 -535
  165. package/.output/server/_ssr/installations-K8TCYTYh.mjs +0 -19
  166. package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +0 -527
  167. package/.output/server/_ssr/table-CU58HeI7.mjs +0 -53
  168. package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +0 -103
  169. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  170. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -123
  171. package/src/components/features/insights/ActivityChart.tsx +0 -150
  172. package/src/components/features/insights/BundleDistribution.tsx +0 -133
  173. package/src/components/features/insights/BundleSelector.tsx +0 -114
  174. package/src/components/features/insights/InsightsCapabilityContext.tsx +0 -24
  175. package/src/components/features/insights/InstallationPagination.tsx +0 -48
  176. package/src/components/features/insights/InstallationSearch.tsx +0 -230
  177. package/src/components/features/insights/UpdateOutcomes.tsx +0 -147
  178. package/src/lib/insights-error.ts +0 -19
  179. package/src/lib/insights-input.ts +0 -187
  180. package/src/lib/insights-overview.ts +0 -170
  181. package/src/lib/server/releaseActivity.ts +0 -27
@@ -1,114 +0,0 @@
1
- import { Button } from "@/components/ui/button";
2
- import {
3
- Combobox,
4
- ComboboxContent,
5
- ComboboxEmpty,
6
- ComboboxInput,
7
- ComboboxItem,
8
- ComboboxList,
9
- ComboboxTrigger,
10
- } from "@/components/ui/combobox";
11
- import { Field, FieldLabel } from "@/components/ui/field";
12
-
13
- type BundleOption = {
14
- readonly bundleId: string;
15
- readonly description: string;
16
- };
17
-
18
- export function BundleSelector({
19
- bundleId,
20
- bundles,
21
- onBundleChange,
22
- }: {
23
- readonly bundleId: string;
24
- readonly bundles: readonly BundleOption[];
25
- readonly onBundleChange: (bundleId: string) => void;
26
- }) {
27
- const selectedBundle =
28
- bundles.find((bundle) => bundle.bundleId === bundleId) ?? null;
29
-
30
- return (
31
- <Field
32
- className="w-full min-w-0 sm:max-w-md"
33
- data-disabled={!bundles.length}
34
- >
35
- <FieldLabel htmlFor="insights-bundle-selector">
36
- Bundle to inspect
37
- </FieldLabel>
38
- <Combobox
39
- autoHighlight
40
- disabled={!bundles.length}
41
- filter={(bundle, query) => {
42
- const normalizedQuery = query.trim().toLowerCase();
43
- return (
44
- bundle.bundleId.toLowerCase().includes(normalizedQuery) ||
45
- bundle.description.toLowerCase().includes(normalizedQuery)
46
- );
47
- }}
48
- isItemEqualToValue={(bundle, value) =>
49
- bundle.bundleId === value.bundleId
50
- }
51
- items={bundles}
52
- itemToStringLabel={(bundle) => bundle.bundleId}
53
- onValueChange={(bundle) => {
54
- if (bundle) onBundleChange(bundle.bundleId);
55
- }}
56
- value={selectedBundle}
57
- >
58
- <ComboboxTrigger
59
- aria-label="Bundle to inspect"
60
- id="insights-bundle-selector"
61
- render={
62
- <Button
63
- className="h-auto min-h-12 w-full min-w-0 justify-between px-3 py-2 text-left"
64
- size="lg"
65
- variant="outline"
66
- />
67
- }
68
- >
69
- {selectedBundle ? (
70
- <span className="flex min-w-0 flex-1 flex-col items-start gap-1">
71
- <code className="max-w-full truncate text-xs/4">
72
- {selectedBundle.bundleId}
73
- </code>
74
- <span className="max-w-full truncate text-xs/4 text-muted-foreground">
75
- {selectedBundle.description}
76
- </span>
77
- </span>
78
- ) : (
79
- <span className="truncate text-muted-foreground">
80
- No bundles available
81
- </span>
82
- )}
83
- </ComboboxTrigger>
84
- <ComboboxContent className="min-w-(--anchor-width)">
85
- <ComboboxInput
86
- aria-label="Search bundles"
87
- placeholder="Search by bundle ID or description"
88
- showClear
89
- showTrigger={false}
90
- />
91
- <ComboboxEmpty>No bundles found.</ComboboxEmpty>
92
- <ComboboxList>
93
- {(bundle) => (
94
- <ComboboxItem
95
- className="min-h-12 px-3 py-2"
96
- key={bundle.bundleId}
97
- value={bundle}
98
- >
99
- <span className="flex min-w-0 flex-col gap-1 pr-5">
100
- <code className="max-w-72 truncate text-xs/4">
101
- {bundle.bundleId}
102
- </code>
103
- <span className="truncate text-xs/4 text-muted-foreground">
104
- {bundle.description}
105
- </span>
106
- </span>
107
- </ComboboxItem>
108
- )}
109
- </ComboboxList>
110
- </ComboboxContent>
111
- </Combobox>
112
- </Field>
113
- );
114
- }
@@ -1,24 +0,0 @@
1
- import { createContext, useContext, type ReactNode } from "react";
2
-
3
- import type { InsightsCapabilityState } from "@/lib/insights-api";
4
-
5
- const InsightsCapabilityContext = createContext<InsightsCapabilityState>({
6
- status: "unresolved",
7
- });
8
-
9
- export function InsightsCapabilityProvider({
10
- children,
11
- value,
12
- }: {
13
- readonly children: ReactNode;
14
- readonly value: InsightsCapabilityState;
15
- }) {
16
- return (
17
- <InsightsCapabilityContext.Provider value={value}>
18
- {children}
19
- </InsightsCapabilityContext.Provider>
20
- );
21
- }
22
-
23
- export const useInsightsCapability = (): InsightsCapabilityState =>
24
- useContext(InsightsCapabilityContext);
@@ -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 {
30
- type InstallationSearchRow,
31
- useInstallationSearchQuery,
32
- } from "@/lib/api";
33
- import { isInsightsQueryEnabled } from "@/lib/insights-api";
34
-
35
- import { useInsightsCapability } from "./InsightsCapabilityContext";
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 = useInsightsCapability();
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
- isInsightsQueryEnabled(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="insights-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="insights-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 { BundleEventInsights } from "@/lib/api";
13
-
14
- import { BundleActivityChart } from "../bundles/BundleActivityChart";
15
- import { InsightsErrorAlert } from "./InsightsErrorAlert";
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: BundleEventInsights;
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
- <InsightsErrorAlert
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
- }
@@ -1,19 +0,0 @@
1
- export type InsightsErrorCopy = {
2
- readonly title: string;
3
- readonly description: string;
4
- };
5
-
6
- export const getInsightsErrorCopy = (
7
- error: Error,
8
- fallbackTitle: string,
9
- ): InsightsErrorCopy => {
10
- const match = /^Bundle event scan exceeded (\d+) rows\.$/.exec(error.message);
11
- if (!match) {
12
- return { title: fallbackTitle, description: error.message };
13
- }
14
- const limit = Number(match[1]);
15
- return {
16
- title: "Insights report limit reached",
17
- description: `This query matched more than ${limit.toLocaleString()} reports. Narrow the query and try again.`,
18
- };
19
- };