@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +1 -0
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +1 -0
- package/.output/public/assets/api-keys-BzKocqRy.js +1 -0
- package/.output/public/assets/api-keys-api-BHcODB7q.js +1 -0
- package/.output/public/assets/card-Dhr_eFYj.js +1 -0
- package/.output/public/assets/{chart-no-axes-combined-FaYMGmQS.js → chart-no-axes-combined-Dy4WJv2U.js} +1 -1
- package/.output/public/assets/composite-Bp98A0Ld.js +1 -0
- package/.output/public/assets/createServerFn-CA8fs7QD.js +3 -0
- package/.output/public/assets/dialog-CUTrcPlB.js +1 -0
- package/.output/public/assets/dist-C_NydL0e.js +1 -0
- package/.output/public/assets/index-DdAz-CXv.js +10 -0
- package/.output/public/assets/input-group-cSOG4wh_.js +1 -0
- package/.output/public/assets/insights-C0PBSZ-n.js +1 -0
- package/.output/public/assets/installations-CE33wgbY.js +2 -0
- package/.output/public/assets/installations-CStZja8x.js +1 -0
- package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +1 -0
- package/.output/public/assets/link-CI8idAUi.js +1 -0
- package/.output/public/assets/loader-circle-BoxLrsOu.js +1 -0
- package/.output/public/assets/{refresh-cw-BkbfZ62O.js → refresh-cw-Bqc72UZG.js} +1 -1
- package/.output/public/assets/{routes-BuoI5-Cr.js → routes-DGVXdzpw.js} +2 -2
- package/.output/public/assets/routes-DTlwOr8I.js +1 -0
- package/.output/public/assets/{shield-check-DE_hVuHu.js → shield-check-uK3MHWJZ.js} +1 -1
- package/.output/public/assets/signing-CMf4aqDO.js +1 -0
- package/.output/public/assets/styles-BCvbtKsm.css +2 -0
- package/.output/public/assets/table-2vcV89QB.js +1 -0
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +1 -0
- package/.output/public/assets/x-DCd0j8c6.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +46 -54
- package/.output/server/_libs/@base-ui/react+[...].mjs +283 -2417
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +52 -4
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
- package/.output/server/_libs/lucide-react.mjs +36 -3
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__react-query.mjs +2 -2
- package/.output/server/_libs/verkit.mjs +20 -15
- package/.output/server/_runtime.mjs +1 -10
- package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +55 -0
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +484 -0
- package/.output/server/_ssr/{api-keys-CpBqHpuC.mjs → api-keys-CZeTPQys.mjs} +9 -11
- package/.output/server/_ssr/{api-keys-api-Czkp57UV.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
- package/.output/server/_ssr/{api-keys-rpc-BTtMDSoR.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
- package/.output/server/_ssr/{api-rpc-BEX8ISai.mjs → api-rpc-tAupfGQm.mjs} +73 -142
- package/.output/server/_ssr/{auth-rpc-BFMEC-TT.mjs → auth-rpc-DUaeOWKK.mjs} +2 -2
- package/.output/server/_ssr/{bundle-signing-rpc-BbCFUQSu.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
- package/.output/server/_ssr/{card-Bsc8mRis.mjs → card-BRiPmi0C.mjs} +25 -3
- package/.output/server/_ssr/{config.server-DQYEr_5X.mjs → config.server-afDuAwaG.mjs} +14 -7
- package/.output/server/_ssr/{createSsrRpc-DhfrxEjg.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
- package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
- package/.output/server/_ssr/{databaseRows-QlCYLi0H.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
- package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
- package/.output/server/_ssr/{dialog-C0vKUp8R.mjs → dialog-DS-nw-pA.mjs} +2 -2
- package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
- package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
- package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
- package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
- package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
- package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
- package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
- package/.output/server/_ssr/{router-BxBEvtF1.mjs → router-B9wG_9f8.mjs} +23 -32
- package/.output/server/_ssr/{routes-BC7rn-tf.mjs → routes-CKrrS2q2.mjs} +3 -3
- package/.output/server/_ssr/{routes-CM5KAwpB.mjs → routes-D7Yh_jfj.mjs} +402 -389
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
- package/.output/server/_ssr/{server-CpzW--lT.mjs → server-D_C2rMzf.mjs} +5 -5
- package/.output/server/_ssr/{signing-jk4HANnl.mjs → signing-Dz1WkZOf.mjs} +5 -6
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/{field-DASCh-5d.mjs → table-CnZmon_Z.mjs} +51 -5
- package/.output/server/_ssr/{uuidv7-CaKuKXrV.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
- package/.output/server/index.mjs +164 -178
- package/lib/index.d.mts +4 -3
- package/lib/vite-internal.mjs +1 -1
- package/lib/vite.d.mts +0 -1
- package/package.json +8 -8
- package/src/components/AppSidebar.tsx +10 -14
- package/src/components/ConsoleAccessPage.tsx +1 -1
- package/src/components/HashValueDisplay.tsx +11 -2
- package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
- package/src/components/features/bundles/BundleMetadata.tsx +1 -20
- package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
- package/src/components/features/insights/EventDetails.tsx +151 -0
- package/src/components/features/insights/EventHistoryCard.tsx +245 -0
- package/src/components/features/insights/EventHistoryList.tsx +63 -0
- package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
- package/src/components/features/insights/InsightsControls.tsx +82 -76
- package/src/components/features/insights/InsightsErrorAlert.tsx +5 -5
- package/src/components/features/insights/InsightsOverview.tsx +156 -161
- package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
- package/src/components/features/insights/InsightsPagination.tsx +52 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +178 -176
- package/src/components/features/insights/InstallationMatchesCard.tsx +111 -65
- package/src/components/features/insights/InstallationPageHeader.tsx +57 -105
- package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/combobox.tsx +1 -0
- package/src/components/ui/skeleton.tsx +4 -1
- package/src/components/ui/tooltip.tsx +1 -1
- package/src/index.ts +7 -2
- package/src/lib/api-rpc.ts +19 -90
- package/src/lib/api.ts +0 -99
- package/src/lib/insights-api.ts +93 -137
- package/src/lib/insights-rpc.ts +69 -199
- package/src/lib/insights-view.ts +15 -0
- package/src/lib/public-bundle-error.ts +19 -0
- package/src/lib/server/releaseReachability.ts +1 -4
- package/src/lib/server/runtime.server.ts +5 -141
- package/src/router.tsx +3 -0
- package/src/routes/-installations-search.ts +25 -12
- package/src/routes/__root.tsx +6 -16
- package/src/routes/index.tsx +21 -81
- package/src/routes/insights.tsx +50 -105
- package/src/routes/installations.tsx +290 -138
- package/src/styles.css +6 -0
- package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +0 -1
- package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +0 -1
- package/.output/public/assets/api-keys-api-DGVTTvTJ.js +0 -1
- package/.output/public/assets/api-keys-z16OZIP4.js +0 -1
- package/.output/public/assets/badge-Br_KDO0J.js +0 -1
- package/.output/public/assets/card-DzuEXvyJ.js +0 -1
- package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
- package/.output/public/assets/dialog-BeAENgez.js +0 -1
- package/.output/public/assets/dist-Bssh_cv0.js +0 -1
- package/.output/public/assets/field-Buq148-A.js +0 -1
- package/.output/public/assets/index-uOpQAIC4.js +0 -10
- package/.output/public/assets/input-group-B3p5UpyW.js +0 -1
- package/.output/public/assets/insights-DJTr7Ozn.js +0 -1
- package/.output/public/assets/insights-api-DVMOoRMS.js +0 -1
- package/.output/public/assets/installations-BY64iIAK.js +0 -2
- package/.output/public/assets/installations-D5v9i3AL.js +0 -1
- package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +0 -1
- package/.output/public/assets/routes-C8fvgETO.js +0 -1
- package/.output/public/assets/search-qsIDvS2I.js +0 -1
- package/.output/public/assets/signing-mJD88Y4C.js +0 -1
- package/.output/public/assets/styles-BZIG7t42.css +0 -2
- package/.output/public/assets/table-RamFEclP.js +0 -1
- package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +0 -42
- package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
- package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +0 -1
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
- package/.output/server/_libs/d3-array.mjs +0 -231
- package/.output/server/_libs/d3-color.mjs +0 -330
- package/.output/server/_libs/d3-format.mjs +0 -252
- package/.output/server/_libs/d3-interpolate.mjs +0 -207
- package/.output/server/_libs/d3-path.mjs +0 -77
- package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
- package/.output/server/_libs/d3-shape.mjs +0 -818
- package/.output/server/_libs/decimal.js-light.mjs +0 -884
- package/.output/server/_libs/es-toolkit.mjs +0 -1134
- package/.output/server/_libs/eventemitter3.mjs +0 -236
- package/.output/server/_libs/immer.mjs +0 -532
- package/.output/server/_libs/react-redux+[...].mjs +0 -205
- package/.output/server/_libs/recharts+[...].mjs +0 -14117
- package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +0 -288
- package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +0 -13
- package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +0 -15
- package/.output/server/_ssr/badge-CpWXQwyF.mjs +0 -26
- package/.output/server/_ssr/input-group-etIsBUqv.mjs +0 -288
- package/.output/server/_ssr/insights-api-C1F7UreR.mjs +0 -71
- package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +0 -96
- package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +0 -155
- package/.output/server/_ssr/insights-ygT_aIfo.mjs +0 -858
- package/.output/server/_ssr/installations-DqRT1QTd.mjs +0 -535
- package/.output/server/_ssr/installations-K8TCYTYh.mjs +0 -19
- package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +0 -527
- package/.output/server/_ssr/table-CU58HeI7.mjs +0 -53
- package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +0 -103
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
- package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -123
- package/src/components/features/insights/ActivityChart.tsx +0 -150
- package/src/components/features/insights/BundleDistribution.tsx +0 -133
- package/src/components/features/insights/BundleSelector.tsx +0 -114
- package/src/components/features/insights/InsightsCapabilityContext.tsx +0 -24
- package/src/components/features/insights/InstallationPagination.tsx +0 -48
- package/src/components/features/insights/InstallationSearch.tsx +0 -230
- package/src/components/features/insights/UpdateOutcomes.tsx +0 -147
- package/src/lib/insights-error.ts +0 -19
- package/src/lib/insights-input.ts +0 -187
- package/src/lib/insights-overview.ts +0 -170
- package/src/lib/server/releaseActivity.ts +0 -27
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { useInsightsCapability } from "@/components/features/insights/InsightsCapabilityContext";
|
|
2
|
-
import { InsightsErrorAlert } from "@/components/features/insights/InsightsErrorAlert";
|
|
3
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
5
|
-
import { useBundleEventInsightsQuery } from "@/lib/api";
|
|
6
|
-
|
|
7
|
-
import { BundleActivityChart } from "./BundleActivityChart";
|
|
8
|
-
|
|
9
|
-
interface BundleInsightsSummaryProps {
|
|
10
|
-
readonly bundleId: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function Metric({
|
|
14
|
-
label,
|
|
15
|
-
tone,
|
|
16
|
-
value,
|
|
17
|
-
}: {
|
|
18
|
-
readonly label: string;
|
|
19
|
-
readonly tone: "applied" | "recovered";
|
|
20
|
-
readonly value: number;
|
|
21
|
-
}) {
|
|
22
|
-
return (
|
|
23
|
-
<div className="flex flex-col gap-1">
|
|
24
|
-
<dt className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
25
|
-
<span
|
|
26
|
-
aria-hidden="true"
|
|
27
|
-
className={
|
|
28
|
-
tone === "applied"
|
|
29
|
-
? "size-2 rounded-full bg-chart-2"
|
|
30
|
-
: "size-2 rounded-full bg-muted-foreground"
|
|
31
|
-
}
|
|
32
|
-
/>
|
|
33
|
-
{label}
|
|
34
|
-
</dt>
|
|
35
|
-
<dd className="text-xl font-semibold tracking-tight tabular-nums">
|
|
36
|
-
{value}
|
|
37
|
-
</dd>
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function SupportedBundleInsightsSummary({
|
|
43
|
-
bundleId,
|
|
44
|
-
}: BundleInsightsSummaryProps) {
|
|
45
|
-
const { data, error, isLoading } = useBundleEventInsightsQuery(
|
|
46
|
-
{
|
|
47
|
-
bundleId,
|
|
48
|
-
window: "30d",
|
|
49
|
-
limit: 1,
|
|
50
|
-
offset: 0,
|
|
51
|
-
},
|
|
52
|
-
true,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<Card>
|
|
57
|
-
<CardHeader className="p-4 pb-3">
|
|
58
|
-
<CardTitle className="text-sm font-medium">
|
|
59
|
-
Activity · 30 days
|
|
60
|
-
</CardTitle>
|
|
61
|
-
</CardHeader>
|
|
62
|
-
<CardContent className="flex flex-col gap-3 px-4 pb-4">
|
|
63
|
-
{isLoading ? (
|
|
64
|
-
<div
|
|
65
|
-
aria-label="Loading reported bundle outcomes"
|
|
66
|
-
className="flex flex-col gap-3"
|
|
67
|
-
>
|
|
68
|
-
<div className="grid grid-cols-2 gap-4">
|
|
69
|
-
<Skeleton className="h-10 w-full" />
|
|
70
|
-
<Skeleton className="h-10 w-full" />
|
|
71
|
-
</div>
|
|
72
|
-
<Skeleton className="h-24 w-full sm:h-32" />
|
|
73
|
-
</div>
|
|
74
|
-
) : error ? (
|
|
75
|
-
<InsightsErrorAlert
|
|
76
|
-
error={
|
|
77
|
-
error instanceof Error
|
|
78
|
-
? error
|
|
79
|
-
: new Error("Failed to load bundle insights.")
|
|
80
|
-
}
|
|
81
|
-
fallbackTitle="Insights unavailable"
|
|
82
|
-
/>
|
|
83
|
-
) : (
|
|
84
|
-
<>
|
|
85
|
-
<dl className="grid grid-cols-2 divide-x divide-border/70">
|
|
86
|
-
<div className="pr-4">
|
|
87
|
-
<Metric
|
|
88
|
-
label="Applied"
|
|
89
|
-
tone="applied"
|
|
90
|
-
value={data?.summary.installed ?? 0}
|
|
91
|
-
/>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="pl-4">
|
|
94
|
-
<Metric
|
|
95
|
-
label="Recovered"
|
|
96
|
-
tone="recovered"
|
|
97
|
-
value={data?.summary.recovered ?? 0}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
</dl>
|
|
101
|
-
<BundleActivityChart
|
|
102
|
-
installed={data?.series.installed ?? []}
|
|
103
|
-
recovered={data?.series.recovered ?? []}
|
|
104
|
-
window="30d"
|
|
105
|
-
/>
|
|
106
|
-
</>
|
|
107
|
-
)}
|
|
108
|
-
</CardContent>
|
|
109
|
-
</Card>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function BundleInsightsSummary({
|
|
114
|
-
bundleId,
|
|
115
|
-
}: BundleInsightsSummaryProps) {
|
|
116
|
-
const capability = useInsightsCapability();
|
|
117
|
-
|
|
118
|
-
if (capability.status !== "supported") {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return <SupportedBundleInsightsSummary bundleId={bundleId} />;
|
|
123
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ActiveInstallationOverview,
|
|
3
|
-
ActiveInstallationWindow,
|
|
4
|
-
} from "@hot-updater/server";
|
|
5
|
-
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type ChartConfig,
|
|
9
|
-
ChartContainer,
|
|
10
|
-
ChartTooltip,
|
|
11
|
-
ChartTooltipContent,
|
|
12
|
-
} from "@/components/ui/chart";
|
|
13
|
-
|
|
14
|
-
type ActivitySeries = ActiveInstallationOverview["series"];
|
|
15
|
-
|
|
16
|
-
const dayFormatter = new Intl.DateTimeFormat("en", {
|
|
17
|
-
day: "numeric",
|
|
18
|
-
month: "short",
|
|
19
|
-
timeZone: "UTC",
|
|
20
|
-
});
|
|
21
|
-
const hourFormatter = new Intl.DateTimeFormat("en", {
|
|
22
|
-
day: "numeric",
|
|
23
|
-
hour: "numeric",
|
|
24
|
-
month: "short",
|
|
25
|
-
timeZone: "UTC",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const formatBucket = (
|
|
29
|
-
bucketStartMs: number,
|
|
30
|
-
window: ActiveInstallationWindow,
|
|
31
|
-
): string =>
|
|
32
|
-
(window === "24h" ? hourFormatter : dayFormatter).format(
|
|
33
|
-
new Date(bucketStartMs),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const chartConfig = {
|
|
37
|
-
installations: {
|
|
38
|
-
label: "Active installations",
|
|
39
|
-
color: "var(--chart-2)",
|
|
40
|
-
},
|
|
41
|
-
} satisfies ChartConfig;
|
|
42
|
-
|
|
43
|
-
export function ActivityChart({
|
|
44
|
-
series,
|
|
45
|
-
window,
|
|
46
|
-
}: {
|
|
47
|
-
readonly series: ActivitySeries;
|
|
48
|
-
readonly window: ActiveInstallationWindow;
|
|
49
|
-
}) {
|
|
50
|
-
const chartData = series.map((point) => ({
|
|
51
|
-
bucketStartMs: point.bucketStartMs,
|
|
52
|
-
installations: point.value,
|
|
53
|
-
}));
|
|
54
|
-
const hasReports = series.some(({ value }) => value > 0);
|
|
55
|
-
const bucketName = window === "24h" ? "hour" : "day";
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div className="flex min-w-0 flex-col gap-3">
|
|
59
|
-
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
|
60
|
-
<div>
|
|
61
|
-
<h3 className="text-sm font-medium">Activity over time</h3>
|
|
62
|
-
<p className="text-xs text-muted-foreground">
|
|
63
|
-
Unique active installations per {bucketName}
|
|
64
|
-
</p>
|
|
65
|
-
</div>
|
|
66
|
-
<span className="text-xs text-muted-foreground">UTC</span>
|
|
67
|
-
</div>
|
|
68
|
-
{!hasReports && (
|
|
69
|
-
<p className="text-sm text-muted-foreground">
|
|
70
|
-
No installations reported during this period.
|
|
71
|
-
</p>
|
|
72
|
-
)}
|
|
73
|
-
<ChartContainer
|
|
74
|
-
aria-label={`Active installations per ${bucketName}`}
|
|
75
|
-
className="h-64 w-full aspect-auto"
|
|
76
|
-
config={chartConfig}
|
|
77
|
-
role="img"
|
|
78
|
-
>
|
|
79
|
-
<AreaChart
|
|
80
|
-
accessibilityLayer
|
|
81
|
-
data={chartData}
|
|
82
|
-
margin={{ left: -16, right: 4, top: 8 }}
|
|
83
|
-
>
|
|
84
|
-
<CartesianGrid vertical={false} />
|
|
85
|
-
<XAxis
|
|
86
|
-
axisLine={false}
|
|
87
|
-
dataKey="bucketStartMs"
|
|
88
|
-
minTickGap={28}
|
|
89
|
-
tickFormatter={(value: number) => formatBucket(value, window)}
|
|
90
|
-
tickLine={false}
|
|
91
|
-
/>
|
|
92
|
-
<YAxis
|
|
93
|
-
allowDecimals={false}
|
|
94
|
-
axisLine={false}
|
|
95
|
-
tickLine={false}
|
|
96
|
-
width={32}
|
|
97
|
-
/>
|
|
98
|
-
<ChartTooltip
|
|
99
|
-
content={
|
|
100
|
-
<ChartTooltipContent
|
|
101
|
-
labelFormatter={(_, payload) => {
|
|
102
|
-
const bucket = payload[0]?.payload.bucketStartMs;
|
|
103
|
-
return typeof bucket === "number"
|
|
104
|
-
? formatBucket(bucket, window)
|
|
105
|
-
: "";
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
}
|
|
109
|
-
/>
|
|
110
|
-
<Area
|
|
111
|
-
dataKey="installations"
|
|
112
|
-
fill="var(--color-installations)"
|
|
113
|
-
fillOpacity={0.12}
|
|
114
|
-
isAnimationActive={false}
|
|
115
|
-
stroke="var(--color-installations)"
|
|
116
|
-
strokeWidth={2}
|
|
117
|
-
type="monotone"
|
|
118
|
-
/>
|
|
119
|
-
</AreaChart>
|
|
120
|
-
</ChartContainer>
|
|
121
|
-
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
122
|
-
Each point counts an installation once in that {bucketName}. The total
|
|
123
|
-
above counts it once across the whole period, so the points do not add
|
|
124
|
-
up to the total.
|
|
125
|
-
</p>
|
|
126
|
-
<div className="sr-only">
|
|
127
|
-
<table aria-label={`Exact active installations per ${bucketName}`}>
|
|
128
|
-
<caption>
|
|
129
|
-
Unique installations that reported an update status in each UTC
|
|
130
|
-
{` ${bucketName}`}.
|
|
131
|
-
</caption>
|
|
132
|
-
<thead>
|
|
133
|
-
<tr>
|
|
134
|
-
<th scope="col">UTC {bucketName}</th>
|
|
135
|
-
<th scope="col">Active installations</th>
|
|
136
|
-
</tr>
|
|
137
|
-
</thead>
|
|
138
|
-
<tbody>
|
|
139
|
-
{chartData.map((point) => (
|
|
140
|
-
<tr key={point.bucketStartMs}>
|
|
141
|
-
<th scope="row">{formatBucket(point.bucketStartMs, window)}</th>
|
|
142
|
-
<td>{point.installations}</td>
|
|
143
|
-
</tr>
|
|
144
|
-
))}
|
|
145
|
-
</tbody>
|
|
146
|
-
</table>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { ActiveInstallationOverview } from "@hot-updater/server";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { Progress } from "@/components/ui/progress";
|
|
6
|
-
import {
|
|
7
|
-
Table,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableCell,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from "@/components/ui/table";
|
|
14
|
-
import type { InsightsOverview } from "@/lib/insights-overview";
|
|
15
|
-
|
|
16
|
-
const PAGE_SIZE = 8;
|
|
17
|
-
|
|
18
|
-
const percentage = (value: number): string =>
|
|
19
|
-
new Intl.NumberFormat(undefined, {
|
|
20
|
-
maximumFractionDigits: 1,
|
|
21
|
-
style: "percent",
|
|
22
|
-
}).format(value);
|
|
23
|
-
|
|
24
|
-
export function BundleDistribution({
|
|
25
|
-
active,
|
|
26
|
-
catalog,
|
|
27
|
-
}: {
|
|
28
|
-
readonly active: ActiveInstallationOverview;
|
|
29
|
-
readonly catalog: InsightsOverview;
|
|
30
|
-
}) {
|
|
31
|
-
const [page, setPage] = useState(1);
|
|
32
|
-
const metadata = new Map(
|
|
33
|
-
catalog.configuredRollouts.map(({ bundle, bundleId }) => [
|
|
34
|
-
bundleId,
|
|
35
|
-
bundle,
|
|
36
|
-
]),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (active.bundles.length === 0) {
|
|
40
|
-
return (
|
|
41
|
-
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
42
|
-
No observed installations in this range.
|
|
43
|
-
</p>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const totalPages = Math.ceil(active.bundles.length / PAGE_SIZE);
|
|
48
|
-
const currentPage = Math.min(page, totalPages);
|
|
49
|
-
const start = (currentPage - 1) * PAGE_SIZE;
|
|
50
|
-
const end = Math.min(start + PAGE_SIZE, active.bundles.length);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className="flex min-w-0 flex-col gap-4">
|
|
54
|
-
<div className="overflow-x-auto">
|
|
55
|
-
<Table aria-label="Observed installations by bundle">
|
|
56
|
-
<TableHeader>
|
|
57
|
-
<TableRow className="hover:bg-transparent">
|
|
58
|
-
<TableHead>Bundle</TableHead>
|
|
59
|
-
<TableHead className="hidden w-2/5 sm:table-cell">
|
|
60
|
-
Activity
|
|
61
|
-
</TableHead>
|
|
62
|
-
<TableHead className="text-right">Observed</TableHead>
|
|
63
|
-
<TableHead className="text-right">Share</TableHead>
|
|
64
|
-
</TableRow>
|
|
65
|
-
</TableHeader>
|
|
66
|
-
<TableBody>
|
|
67
|
-
{active.bundles.slice(start, end).map((row) => {
|
|
68
|
-
const bundle = metadata.get(row.bundleId);
|
|
69
|
-
const share = row.installations / active.activeInstallations;
|
|
70
|
-
const shareLabel = percentage(share);
|
|
71
|
-
return (
|
|
72
|
-
<TableRow key={row.bundleId}>
|
|
73
|
-
<TableCell className="max-w-48 whitespace-normal">
|
|
74
|
-
<div className="flex min-w-0 flex-col gap-1">
|
|
75
|
-
<code className="break-all text-xs">{row.bundleId}</code>
|
|
76
|
-
<span className="text-muted-foreground">
|
|
77
|
-
{bundle
|
|
78
|
-
? `${bundle.platform} · ${bundle.channel}`
|
|
79
|
-
: "Unknown bundle metadata"}
|
|
80
|
-
</span>
|
|
81
|
-
</div>
|
|
82
|
-
</TableCell>
|
|
83
|
-
<TableCell className="hidden sm:table-cell">
|
|
84
|
-
<Progress
|
|
85
|
-
aria-label={`${row.bundleId} observed share ${shareLabel}`}
|
|
86
|
-
aria-valuetext={shareLabel}
|
|
87
|
-
className="h-2"
|
|
88
|
-
value={share * 100}
|
|
89
|
-
/>
|
|
90
|
-
</TableCell>
|
|
91
|
-
<TableCell className="text-right tabular-nums">
|
|
92
|
-
{row.installations.toLocaleString()}
|
|
93
|
-
</TableCell>
|
|
94
|
-
<TableCell className="text-right tabular-nums">
|
|
95
|
-
{shareLabel}
|
|
96
|
-
</TableCell>
|
|
97
|
-
</TableRow>
|
|
98
|
-
);
|
|
99
|
-
})}
|
|
100
|
-
</TableBody>
|
|
101
|
-
</Table>
|
|
102
|
-
</div>
|
|
103
|
-
{totalPages > 1 && (
|
|
104
|
-
<div className="flex flex-wrap items-center justify-between gap-2 px-4 pb-4">
|
|
105
|
-
<p className="text-xs text-muted-foreground tabular-nums">
|
|
106
|
-
Showing {start + 1}–{end} of{" "}
|
|
107
|
-
{active.bundles.length.toLocaleString()}
|
|
108
|
-
</p>
|
|
109
|
-
<div className="flex gap-2">
|
|
110
|
-
<Button
|
|
111
|
-
disabled={currentPage === 1}
|
|
112
|
-
onClick={() => setPage(currentPage - 1)}
|
|
113
|
-
size="sm"
|
|
114
|
-
type="button"
|
|
115
|
-
variant="outline"
|
|
116
|
-
>
|
|
117
|
-
Previous
|
|
118
|
-
</Button>
|
|
119
|
-
<Button
|
|
120
|
-
disabled={currentPage === totalPages}
|
|
121
|
-
onClick={() => setPage(currentPage + 1)}
|
|
122
|
-
size="sm"
|
|
123
|
-
type="button"
|
|
124
|
-
variant="outline"
|
|
125
|
-
>
|
|
126
|
-
Next
|
|
127
|
-
</Button>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
);
|
|
133
|
-
}
|