@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.4
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/InsightsPageHeader-C-Xc2MB9.js +1 -0
- package/.output/public/assets/api-keys-C1sgC6ko.js +1 -0
- package/.output/public/assets/{api-keys-api-BHcODB7q.js → api-keys-api-LchP65vQ.js} +1 -1
- package/.output/public/assets/badge-BuMDU0Rf.js +1 -0
- package/.output/public/assets/card-BEvx_QKh.js +1 -0
- package/.output/public/assets/{chart-no-axes-combined-Dy4WJv2U.js → chart-no-axes-combined-C0G0fQ3J.js} +1 -1
- package/.output/public/assets/{composite-Bp98A0Ld.js → composite-CHTH1Xhh.js} +1 -1
- package/.output/public/assets/createServerFn-DHc2erub.js +3 -0
- package/.output/public/assets/dialog-dyZ1SphZ.js +1 -0
- package/.output/public/assets/{dist-C_NydL0e.js → dist-Bh1gaDfB.js} +1 -1
- package/.output/public/assets/field-79SVH26s.js +1 -0
- package/.output/public/assets/index-BNkxg4jz.js +10 -0
- package/.output/public/assets/input-group-BbHfaBzQ.js +1 -0
- package/.output/public/assets/insights-D_7uSKfc.js +1 -0
- package/.output/public/assets/installations-BzoHWuXw.js +1 -0
- package/.output/public/assets/{installations-CE33wgbY.js → installations-DvjbUqd1.js} +2 -2
- package/.output/public/assets/{lazyRouteComponent-Ccp3bwBY.js → lazyRouteComponent-BN0MS05o.js} +1 -1
- package/.output/public/assets/{link-CI8idAUi.js → link-BG06whwB.js} +1 -1
- package/.output/public/assets/{loader-circle-BoxLrsOu.js → loader-circle-Bbp8LGVC.js} +1 -1
- package/.output/public/assets/{refresh-cw-Bqc72UZG.js → refresh-cw-DZgiAOMk.js} +1 -1
- package/.output/public/assets/routes-BFE-oRIT.js +1 -0
- package/.output/public/assets/{routes-DGVXdzpw.js → routes-DaPMY31P.js} +2 -2
- package/.output/public/assets/{shield-check-uK3MHWJZ.js → shield-check-DoIiaaXU.js} +1 -1
- package/.output/public/assets/signing-BTRhaF5D.js +1 -0
- package/.output/public/assets/styles-C7U0oVke.css +2 -0
- package/.output/public/assets/table-BBEzJ7Xl.js +1 -0
- package/.output/public/assets/useCompositeListItem-BkPDaqyj.js +42 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-Dr2ixDgQ.mjs} +40 -32
- package/.output/server/_libs/@base-ui/react+[...].mjs +1 -1
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +1 -1
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1159 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +9 -2
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +15054 -0
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__react-query.mjs +1 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/InsightsPageHeader-C5Has8Ry.mjs +60 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-DJml_XoK.mjs} +10 -8
- package/.output/server/_ssr/{api-keys-api-BUPQVSpk.mjs → api-keys-api-CrCZyvnl.mjs} +2 -2
- package/.output/server/_ssr/{api-keys-rpc-Cs7tS5_j.mjs → api-keys-rpc-CFaNf-q0.mjs} +3 -3
- package/.output/server/_ssr/{api-rpc-tAupfGQm.mjs → api-rpc-B61KL3Eo.mjs} +25 -25
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-D_hv52-J.mjs} +4 -4
- package/.output/server/_ssr/{auth.server-DW71WzsD.mjs → auth.server-DF_shCeq.mjs} +1 -1
- package/.output/server/_ssr/badge-BgsCP9Dp.mjs +28 -0
- package/.output/server/_ssr/{bundle-signing-rpc-BhnBw0-d.mjs → bundle-signing-rpc-BBipZnFs.mjs} +2 -2
- package/.output/server/_ssr/{bundle-signing.server-C4NW5mWx.mjs → bundle-signing.server-Co_zHdJx.mjs} +1 -1
- package/.output/server/_ssr/bundleActivity-D3_fIda2.mjs +27 -0
- package/.output/server/_ssr/{card-BRiPmi0C.mjs → card-DrbxCPtG.mjs} +3 -25
- package/.output/server/_ssr/chart-qxa-_30a.mjs +162 -0
- package/.output/server/_ssr/{config.server-afDuAwaG.mjs → config.server-CBr6PDj5.mjs} +5 -5
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → createSsrRpc-NHF2PxuK.mjs} +4 -4
- package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-BAq3JuGr.mjs} +1 -1
- package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-DUaIgpgd.mjs} +1 -1
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-B3ert-pY.mjs} +1 -1
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-CqNUlqUq.mjs} +4 -50
- package/.output/server/_ssr/{getBundleChildren-C0vh4ZvE.mjs → getBundleChildren-COMt16m_.mjs} +1 -1
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-Bb8ueylQ.mjs} +51 -2
- package/.output/server/_ssr/insights-B-DUbksX.mjs +502 -0
- package/.output/server/_ssr/insights-recovery-DSFM-YTL.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-rpc-P71FH66n.mjs +37 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrIQCjpD.mjs} +2 -2
- package/.output/server/_ssr/insightsRecovery-CqcosG7B.mjs +149 -0
- package/.output/server/_ssr/{installations-CtA59MQG.mjs → installations-BQrppti7.mjs} +2 -2
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-YumqpNpa.mjs} +426 -11
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-iP95Vqi2.mjs} +14 -14
- package/.output/server/_ssr/{routes-CKrrS2q2.mjs → routes-BDabsmc7.mjs} +2 -2
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-ivsV2TDu.mjs} +224 -13
- package/.output/server/_ssr/{runtime.server-DacU1TBV.mjs → runtime.server-jmZOKIQM.mjs} +1 -1
- package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-p7yrVBfD.mjs} +3 -3
- package/.output/server/_ssr/{signing-Dz1WkZOf.mjs → signing-CxCbnFCt.mjs} +5 -4
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/table-D1mL8fJu.mjs +53 -0
- package/.output/server/_tanstack-start-manifest_v-Cwpu0C9Q.mjs +100 -0
- package/.output/server/index.mjs +154 -154
- package/package.json +3 -3
- package/src/components/features/bundles/BundleActivityChart.tsx +95 -0
- package/src/components/features/bundles/BundleInsightsSummary.tsx +121 -0
- package/src/components/features/insights/InsightsControls.tsx +1 -15
- package/src/components/features/insights/InsightsOverview.tsx +368 -179
- package/src/components/features/releases/ReleaseEditorSheet.tsx +10 -0
- package/src/components/ui/empty.tsx +101 -0
- package/src/lib/bundle-activity.ts +21 -0
- package/src/lib/insights-recovery-rpc.ts +38 -0
- package/src/lib/insights-recovery.ts +59 -0
- package/src/lib/server/bundleActivity.ts +37 -0
- package/src/lib/server/insightsHistory.ts +52 -0
- package/src/lib/server/insightsRecovery.ts +186 -0
- package/src/routes/index.tsx +33 -3
- package/src/routes/insights.tsx +1 -42
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
- package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
- package/.output/public/assets/card-Dhr_eFYj.js +0 -1
- package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
- package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
- package/.output/public/assets/index-DdAz-CXv.js +0 -10
- package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
- package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
- package/.output/public/assets/installations-CStZja8x.js +0 -1
- package/.output/public/assets/routes-DTlwOr8I.js +0 -1
- package/.output/public/assets/signing-CMf4aqDO.js +0 -1
- package/.output/public/assets/styles-BCvbtKsm.css +0 -2
- package/.output/public/assets/table-2vcV89QB.js +0 -1
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
- package/.output/public/assets/x-DCd0j8c6.js +0 -1
- package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +0 -55
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- /package/.output/public/assets/{preload-helper-CQ36nxok.js → preload-helper-uTix4PVD.js} +0 -0
|
@@ -1,203 +1,392 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
1
2
|
import { Link } from "@tanstack/react-router";
|
|
2
|
-
import { ArrowUpRight } from "lucide-react";
|
|
3
|
+
import { ArrowUpRight, RotateCw, TriangleAlert } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
|
3
6
|
|
|
4
7
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
5
8
|
import {
|
|
6
9
|
Card,
|
|
7
10
|
CardContent,
|
|
8
|
-
CardDescription,
|
|
9
11
|
CardFooter,
|
|
10
12
|
CardHeader,
|
|
11
13
|
CardTitle,
|
|
12
14
|
} from "@/components/ui/card";
|
|
15
|
+
import {
|
|
16
|
+
ChartContainer,
|
|
17
|
+
ChartTooltip,
|
|
18
|
+
ChartTooltipContent,
|
|
19
|
+
} from "@/components/ui/chart";
|
|
13
20
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
21
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
22
|
+
import type { RecoveryInput, RecoveryReport } from "@/lib/insights-recovery";
|
|
23
|
+
import { getRecoveryReportRpc } from "@/lib/insights-recovery-rpc";
|
|
16
24
|
|
|
17
|
-
import { EventTimestamp, useInsightsTimeFormat } from "./EventDetails";
|
|
18
|
-
import type { InsightsReportSelection } from "./InsightsBundleReports";
|
|
19
25
|
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<Card aria-label="Loading reporting installations" className="shadow-sm">
|
|
42
|
-
<CardHeader className="px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
|
|
43
|
-
<Skeleton className="h-4 w-40" />
|
|
44
|
-
<Skeleton className="h-3 w-full max-w-md" />
|
|
45
|
-
</CardHeader>
|
|
46
|
-
<CardContent className="px-4 pb-6 sm:px-6">
|
|
47
|
-
<Skeleton className="h-12 w-28" />
|
|
48
|
-
</CardContent>
|
|
49
|
-
<CardFooter className="border-t px-4 py-3 sm:px-6">
|
|
50
|
-
<Skeleton className="h-8 w-full max-w-sm" />
|
|
51
|
-
</CardFooter>
|
|
52
|
-
</Card>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (props.status === "error") {
|
|
57
|
-
return (
|
|
58
|
-
<InsightsErrorAlert
|
|
59
|
-
error={props.error}
|
|
60
|
-
fallbackTitle="Reporting installations unavailable"
|
|
61
|
-
/>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const { active } = props;
|
|
66
|
-
const bundle = active.bundle;
|
|
27
|
+
const dates = new Intl.DateTimeFormat("en", {
|
|
28
|
+
month: "short",
|
|
29
|
+
day: "numeric",
|
|
30
|
+
timeZone: "UTC",
|
|
31
|
+
});
|
|
32
|
+
const times = new Intl.DateTimeFormat("en", {
|
|
33
|
+
month: "short",
|
|
34
|
+
day: "numeric",
|
|
35
|
+
hour: "numeric",
|
|
36
|
+
timeZone: "UTC",
|
|
37
|
+
});
|
|
38
|
+
const colors = [
|
|
39
|
+
"var(--chart-2)",
|
|
40
|
+
"var(--foreground)",
|
|
41
|
+
"var(--chart-1)",
|
|
42
|
+
"var(--muted-foreground)",
|
|
43
|
+
"var(--chart-4)",
|
|
44
|
+
];
|
|
45
|
+
const dashes = [undefined, "6 3", "2 3"];
|
|
67
46
|
|
|
47
|
+
export function ActivityChart({ report }: { readonly report: RecoveryReport }) {
|
|
48
|
+
const [metric, setMetric] = useState<"active" | "rollback">("active");
|
|
49
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
50
|
+
const selected = report.series.find(
|
|
51
|
+
(series) => series.releaseId === selectedId,
|
|
52
|
+
);
|
|
53
|
+
const config = Object.fromEntries(
|
|
54
|
+
report.series.map((series, index) => [
|
|
55
|
+
`bundle${index}`,
|
|
56
|
+
{
|
|
57
|
+
label: series.releaseId,
|
|
58
|
+
color: colors[index % colors.length],
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
);
|
|
62
|
+
const data = (report.series[0]?.points ?? []).map((point, pointIndex) => ({
|
|
63
|
+
startMs: point.startMs,
|
|
64
|
+
...Object.fromEntries(
|
|
65
|
+
report.series.map((series, index) => [
|
|
66
|
+
`bundle${index}`,
|
|
67
|
+
metric === "active"
|
|
68
|
+
? series.points[pointIndex].active
|
|
69
|
+
: series.points[pointIndex].recoveredInstallations,
|
|
70
|
+
]),
|
|
71
|
+
),
|
|
72
|
+
}));
|
|
73
|
+
const formatTick = (ms: number) =>
|
|
74
|
+
(report.intervalMs < 86_400_000 ? times : dates).format(ms);
|
|
75
|
+
const lastSpike = selected
|
|
76
|
+
? [...selected.points].reverse().find((point) => point.spike)
|
|
77
|
+
: undefined;
|
|
78
|
+
const total =
|
|
79
|
+
metric === "active"
|
|
80
|
+
? report.series.reduce(
|
|
81
|
+
(sum, series) => sum + series.activeInstallations,
|
|
82
|
+
0,
|
|
83
|
+
)
|
|
84
|
+
: report.series.filter((series) => series.recoveredInstallations > 0)
|
|
85
|
+
.length;
|
|
68
86
|
return (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<h2 id="reporting-installations-heading">
|
|
77
|
-
Reporting installations
|
|
78
|
-
</h2>
|
|
79
|
-
</CardTitle>
|
|
80
|
-
<CardDescription>
|
|
81
|
-
{active.platform === "ios" ? "iOS" : "Android"} · {active.channel} ·{" "}
|
|
82
|
-
{windowCopy[active.window]}. Installations are counted by their last
|
|
83
|
-
report received.
|
|
84
|
-
</CardDescription>
|
|
85
|
-
</CardHeader>
|
|
86
|
-
<CardContent className="px-4 pt-2 pb-5 sm:px-6 sm:pb-6">
|
|
87
|
-
<p className="text-4xl font-semibold tracking-tight tabular-nums sm:text-5xl">
|
|
88
|
-
{active.reportingInstallations.count.toLocaleString()}
|
|
87
|
+
<>
|
|
88
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
89
|
+
<div>
|
|
90
|
+
<p className="text-sm text-muted-foreground">
|
|
91
|
+
{metric === "active"
|
|
92
|
+
? "Active installations"
|
|
93
|
+
: "Bundles with rollbacks"}
|
|
89
94
|
</p>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
95
|
+
<p className="mt-1 text-4xl font-semibold tracking-tight tabular-nums">
|
|
96
|
+
{total.toLocaleString()}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
<ToggleGroup
|
|
100
|
+
aria-label="Activity metric"
|
|
101
|
+
value={[metric]}
|
|
102
|
+
onValueChange={(values) => {
|
|
103
|
+
if (values[0] === "active" || values[0] === "rollback")
|
|
104
|
+
setMetric(values[0]);
|
|
105
|
+
}}
|
|
106
|
+
spacing={0}
|
|
107
|
+
variant="outline"
|
|
108
|
+
>
|
|
109
|
+
<ToggleGroupItem className="h-11 lg:h-8" value="active">
|
|
110
|
+
Active
|
|
111
|
+
</ToggleGroupItem>
|
|
112
|
+
<ToggleGroupItem className="h-11 lg:h-8" value="rollback">
|
|
113
|
+
Rollback
|
|
114
|
+
</ToggleGroupItem>
|
|
115
|
+
</ToggleGroup>
|
|
116
|
+
</div>
|
|
117
|
+
{report.series.length === 0 ? (
|
|
118
|
+
<div className="flex h-48 items-center justify-center text-center text-sm text-muted-foreground">
|
|
119
|
+
No bundle reports in this period. Try another channel or check again
|
|
120
|
+
after the app reports activity.
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<div>
|
|
125
|
+
<p className="mb-2 text-right text-xs text-muted-foreground">
|
|
126
|
+
{metric === "active"
|
|
127
|
+
? "Installations"
|
|
128
|
+
: "Recovered installations per interval"}{" "}
|
|
129
|
+
· UTC
|
|
130
|
+
</p>
|
|
131
|
+
<ChartContainer
|
|
132
|
+
aria-label={`${metric === "active" ? "Active" : "Rollback"} trend for all reported bundle IDs`}
|
|
133
|
+
className="h-48 w-full aspect-auto sm:h-56"
|
|
134
|
+
config={config}
|
|
135
|
+
>
|
|
136
|
+
<LineChart
|
|
137
|
+
accessibilityLayer
|
|
138
|
+
data={data}
|
|
139
|
+
margin={{ left: -12, right: 12, top: 8 }}
|
|
140
|
+
>
|
|
141
|
+
<CartesianGrid vertical={false} />
|
|
142
|
+
<XAxis
|
|
143
|
+
axisLine={false}
|
|
144
|
+
dataKey="startMs"
|
|
145
|
+
minTickGap={40}
|
|
146
|
+
tickFormatter={formatTick}
|
|
147
|
+
tickLine={false}
|
|
148
|
+
/>
|
|
149
|
+
<YAxis
|
|
150
|
+
allowDecimals={false}
|
|
151
|
+
axisLine={false}
|
|
152
|
+
tickLine={false}
|
|
153
|
+
width={48}
|
|
154
|
+
/>
|
|
155
|
+
<ChartTooltip
|
|
156
|
+
content={
|
|
157
|
+
<ChartTooltipContent
|
|
158
|
+
className="max-h-64 max-w-[calc(100vw-3rem)] overflow-auto"
|
|
159
|
+
labelFormatter={(_, payload) =>
|
|
160
|
+
`${times.format(payload[0]?.payload.startMs)} · UTC`
|
|
161
|
+
}
|
|
162
|
+
/>
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
{report.series.map((series, index) => (
|
|
166
|
+
<Line
|
|
167
|
+
key={series.releaseId}
|
|
168
|
+
dataKey={`bundle${index}`}
|
|
169
|
+
type="linear"
|
|
170
|
+
stroke={`var(--color-bundle${index})`}
|
|
171
|
+
strokeWidth={selectedId === series.releaseId ? 3 : 2}
|
|
172
|
+
strokeOpacity={!selected || selected === series ? 1 : 0.4}
|
|
173
|
+
strokeDasharray={
|
|
174
|
+
dashes[Math.floor(index / colors.length) % dashes.length]
|
|
175
|
+
}
|
|
176
|
+
dot={{ r: 2 }}
|
|
177
|
+
activeDot={{ r: 4 }}
|
|
178
|
+
isAnimationActive={false}
|
|
137
179
|
/>
|
|
138
|
-
|
|
139
|
-
</
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
180
|
+
))}
|
|
181
|
+
</LineChart>
|
|
182
|
+
</ChartContainer>
|
|
183
|
+
</div>
|
|
184
|
+
<div
|
|
185
|
+
aria-label="Reported bundles"
|
|
186
|
+
className="grid max-h-48 grid-cols-1 gap-1 overflow-y-auto sm:grid-cols-2"
|
|
187
|
+
>
|
|
188
|
+
{report.series.map((series, index) => (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
key={series.releaseId}
|
|
192
|
+
aria-label={`Highlight bundle ${series.releaseId}`}
|
|
193
|
+
aria-pressed={selectedId === series.releaseId}
|
|
194
|
+
title={series.releaseId}
|
|
195
|
+
onClick={() =>
|
|
196
|
+
setSelectedId(
|
|
197
|
+
selectedId === series.releaseId ? null : series.releaseId,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-muted"
|
|
201
|
+
>
|
|
202
|
+
<svg
|
|
203
|
+
aria-hidden="true"
|
|
204
|
+
width="20"
|
|
205
|
+
height="8"
|
|
206
|
+
className="shrink-0"
|
|
150
207
|
>
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
touch
|
|
161
|
-
value={measurement.measuredAtMs}
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
<Button
|
|
166
|
-
className="h-11 lg:h-8"
|
|
167
|
-
variant="outline"
|
|
168
|
-
onClick={() =>
|
|
169
|
-
props.onOutcomeSelect({
|
|
170
|
-
bundle: {
|
|
171
|
-
platform: active.platform,
|
|
172
|
-
channel: active.channel,
|
|
173
|
-
bundleId: bundle.bundleId,
|
|
174
|
-
outcome,
|
|
175
|
-
},
|
|
176
|
-
sinceMs: active.sinceMs,
|
|
177
|
-
beforeReceivedAtMs: active.beforeReceivedAtMs,
|
|
178
|
-
})
|
|
208
|
+
<line
|
|
209
|
+
x1="0"
|
|
210
|
+
x2="20"
|
|
211
|
+
y1="4"
|
|
212
|
+
y2="4"
|
|
213
|
+
stroke={colors[index % colors.length]}
|
|
214
|
+
strokeWidth="2"
|
|
215
|
+
strokeDasharray={
|
|
216
|
+
dashes[Math.floor(index / colors.length) % dashes.length]
|
|
179
217
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
</
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
/>
|
|
219
|
+
</svg>
|
|
220
|
+
<span className="min-w-0 truncate font-mono">
|
|
221
|
+
{series.releaseId}
|
|
222
|
+
</span>
|
|
223
|
+
{series.points.some((point) => point.spike) ? (
|
|
224
|
+
<TriangleAlert
|
|
225
|
+
aria-label="Rollback spike"
|
|
226
|
+
className="size-3 shrink-0"
|
|
227
|
+
/>
|
|
228
|
+
) : null}
|
|
229
|
+
<span className="ml-auto tabular-nums">
|
|
230
|
+
{(metric === "active"
|
|
231
|
+
? series.activeInstallations
|
|
232
|
+
: series.recoveredInstallations
|
|
233
|
+
).toLocaleString()}
|
|
234
|
+
</span>
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
{selected ? (
|
|
239
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
|
|
240
|
+
<div className="min-w-0 flex-1">
|
|
241
|
+
<p className="break-all font-mono text-xs">
|
|
242
|
+
{selected.releaseId}
|
|
243
|
+
</p>
|
|
244
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
245
|
+
Active {selected.activeInstallations.toLocaleString()} ·
|
|
246
|
+
Rollback {selected.recoveredInstallations.toLocaleString()} in
|
|
247
|
+
this period
|
|
248
|
+
</p>
|
|
249
|
+
{metric === "rollback" && lastSpike ? (
|
|
250
|
+
<p className="mt-2 flex items-center gap-2 text-xs">
|
|
251
|
+
<TriangleAlert
|
|
252
|
+
className="size-3 shrink-0"
|
|
253
|
+
aria-hidden="true"
|
|
254
|
+
/>
|
|
255
|
+
Rollback spike on {dates.format(lastSpike.startMs)}. Review
|
|
256
|
+
this bundle’s delivery settings.
|
|
257
|
+
</p>
|
|
258
|
+
) : null}
|
|
259
|
+
</div>
|
|
260
|
+
<Link
|
|
261
|
+
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
262
|
+
to="/"
|
|
263
|
+
search={{ releaseId: selected.releaseId }}
|
|
264
|
+
>
|
|
265
|
+
Review bundle
|
|
266
|
+
<ArrowUpRight aria-hidden="true" />
|
|
267
|
+
</Link>
|
|
268
|
+
</div>
|
|
269
|
+
) : null}
|
|
270
|
+
<details className="text-xs text-muted-foreground">
|
|
271
|
+
<summary className="cursor-pointer">View chart data</summary>
|
|
272
|
+
<div className="mt-2 max-h-64 overflow-auto">
|
|
273
|
+
<table className="w-full text-left">
|
|
274
|
+
<caption className="sr-only">
|
|
275
|
+
{metric === "active"
|
|
276
|
+
? "Active installations"
|
|
277
|
+
: "Recovered installations"}{" "}
|
|
278
|
+
by bundle ID, UTC
|
|
279
|
+
</caption>
|
|
280
|
+
<thead>
|
|
281
|
+
<tr>
|
|
282
|
+
<th scope="col" className="pr-4">
|
|
283
|
+
Date (UTC)
|
|
284
|
+
</th>
|
|
285
|
+
{report.series.map((series) => (
|
|
286
|
+
<th
|
|
287
|
+
scope="col"
|
|
288
|
+
className="px-2 font-mono"
|
|
289
|
+
key={series.releaseId}
|
|
290
|
+
>
|
|
291
|
+
{series.releaseId}
|
|
292
|
+
</th>
|
|
293
|
+
))}
|
|
294
|
+
</tr>
|
|
295
|
+
</thead>
|
|
296
|
+
<tbody>
|
|
297
|
+
{data.map((point, i) => (
|
|
298
|
+
<tr key={point.startMs}>
|
|
299
|
+
<th
|
|
300
|
+
scope="row"
|
|
301
|
+
className="whitespace-nowrap pr-4 font-normal"
|
|
302
|
+
>
|
|
303
|
+
{times.format(point.startMs)}
|
|
304
|
+
</th>
|
|
305
|
+
{report.series.map((series) => (
|
|
306
|
+
<td
|
|
307
|
+
className="px-2 tabular-nums"
|
|
308
|
+
key={series.releaseId}
|
|
309
|
+
>
|
|
310
|
+
{(metric === "active"
|
|
311
|
+
? series.points[i].active
|
|
312
|
+
: series.points[i].recoveredInstallations) ?? "—"}
|
|
313
|
+
</td>
|
|
314
|
+
))}
|
|
315
|
+
</tr>
|
|
316
|
+
))}
|
|
317
|
+
</tbody>
|
|
318
|
+
</table>
|
|
319
|
+
</div>
|
|
320
|
+
</details>
|
|
321
|
+
</>
|
|
322
|
+
)}
|
|
323
|
+
</>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function InsightsOverview({ input }: { readonly input: RecoveryInput }) {
|
|
328
|
+
const query = useQuery({
|
|
329
|
+
queryKey: ["rollout-activity", input],
|
|
330
|
+
queryFn: () => getRecoveryReportRpc({ data: input }),
|
|
331
|
+
staleTime: 30_000,
|
|
332
|
+
});
|
|
333
|
+
return (
|
|
334
|
+
<Card className="min-w-0 overflow-hidden shadow-sm">
|
|
335
|
+
<CardHeader className="flex-row items-center justify-between gap-4 px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
|
|
336
|
+
<CardTitle className="text-sm font-medium">
|
|
337
|
+
Bundle activity ·{" "}
|
|
338
|
+
{input.window === "30d"
|
|
339
|
+
? "30 days"
|
|
340
|
+
: input.window === "7d"
|
|
341
|
+
? "7 days"
|
|
342
|
+
: "24 hours"}
|
|
343
|
+
</CardTitle>
|
|
344
|
+
<Button
|
|
345
|
+
aria-label="Refresh bundle activity"
|
|
346
|
+
variant="ghost"
|
|
347
|
+
size="icon-sm"
|
|
348
|
+
disabled={query.isFetching}
|
|
349
|
+
onClick={() => void query.refetch()}
|
|
350
|
+
>
|
|
351
|
+
<RotateCw aria-hidden="true" />
|
|
352
|
+
</Button>
|
|
353
|
+
</CardHeader>
|
|
354
|
+
<CardContent className="flex flex-col gap-4 px-4 pb-4 sm:px-6 sm:pb-6">
|
|
355
|
+
{query.isPending ? (
|
|
356
|
+
<Skeleton
|
|
357
|
+
aria-label="Loading bundle activity"
|
|
358
|
+
className="h-64 w-full"
|
|
359
|
+
/>
|
|
360
|
+
) : query.error ? (
|
|
361
|
+
<InsightsErrorAlert
|
|
362
|
+
error={query.error}
|
|
363
|
+
fallbackTitle="Bundle activity unavailable"
|
|
364
|
+
/>
|
|
365
|
+
) : query.data ? (
|
|
366
|
+
<ActivityChart key={JSON.stringify(input)} report={query.data} />
|
|
367
|
+
) : null}
|
|
368
|
+
</CardContent>
|
|
369
|
+
{query.data && !query.error ? (
|
|
370
|
+
<CardFooter className="flex-col items-start gap-2 border-t bg-muted/15 px-4 py-3 text-xs text-muted-foreground sm:px-6">
|
|
371
|
+
<p>
|
|
372
|
+
Active follows each installation’s last reported ID in this period.
|
|
373
|
+
Rollbacks count installations recovered from each ID per interval.
|
|
374
|
+
</p>
|
|
375
|
+
{query.data.unattributedInstallations > 0 ? (
|
|
376
|
+
<p>
|
|
377
|
+
{query.data.unattributedInstallations.toLocaleString()} reporting
|
|
378
|
+
installations have no observed bundle ID and are excluded.
|
|
379
|
+
</p>
|
|
380
|
+
) : null}
|
|
381
|
+
{query.data.truncated ? (
|
|
382
|
+
<p>
|
|
383
|
+
Partial history — only reports since{" "}
|
|
384
|
+
{times.format(query.data.sinceMs)} UTC are included. Choose a
|
|
385
|
+
shorter period to see more complete counts.
|
|
193
386
|
</p>
|
|
194
|
-
|
|
195
|
-
</
|
|
387
|
+
) : null}
|
|
388
|
+
</CardFooter>
|
|
196
389
|
) : null}
|
|
197
|
-
|
|
198
|
-
Offline installations, opt-outs, and reports that did not reach the
|
|
199
|
-
server are absent.
|
|
200
|
-
</p>
|
|
201
|
-
</section>
|
|
390
|
+
</Card>
|
|
202
391
|
);
|
|
203
392
|
}
|
|
@@ -15,6 +15,7 @@ import { toast } from "sonner";
|
|
|
15
15
|
import { normalizeRange } from "verkit";
|
|
16
16
|
|
|
17
17
|
import { BundleIdDisplay } from "@/components/BundleIdDisplay";
|
|
18
|
+
import { BundleInsightsSummary } from "@/components/features/bundles/BundleInsightsSummary";
|
|
18
19
|
import { BundleMetadata } from "@/components/features/bundles/BundleMetadata";
|
|
19
20
|
import { RolloutCohortsDialog } from "@/components/features/bundles/RolloutCohortsDialog";
|
|
20
21
|
import { PlatformIcon } from "@/components/PlatformIcon";
|
|
@@ -408,6 +409,15 @@ export function ReleaseEditorSheet({
|
|
|
408
409
|
|
|
409
410
|
{release && draft ? (
|
|
410
411
|
<div className="flex flex-col gap-6 px-4 pb-4 sm:px-6 sm:pb-6">
|
|
412
|
+
{channelName ? (
|
|
413
|
+
<BundleInsightsSummary
|
|
414
|
+
input={{
|
|
415
|
+
platform: release.platform,
|
|
416
|
+
channel: channelName,
|
|
417
|
+
releaseId: release.id,
|
|
418
|
+
}}
|
|
419
|
+
/>
|
|
420
|
+
) : null}
|
|
411
421
|
<section
|
|
412
422
|
aria-labelledby="delivery-settings-heading"
|
|
413
423
|
className="flex flex-col gap-4"
|