@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.
- 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-DspNKfMj.js → dist-C_NydL0e.js} +1 -1
- 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-Bqc72UZG.js +1 -0
- package/.output/public/assets/{routes-DcQnVajO.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-DBP9TQ7K.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +44 -52
- package/.output/server/_libs/@base-ui/react+[...].mjs +1906 -4040
- 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-BnM_o2nS.mjs → api-keys-CZeTPQys.mjs} +11 -13
- package/.output/server/_ssr/{api-keys-api-CIywsSF5.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
- package/.output/server/_ssr/{api-keys-rpc-BSp1wkIV.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
- package/.output/server/_ssr/{api-rpc-cG5wd_tO.mjs → api-rpc-tAupfGQm.mjs} +73 -142
- package/.output/server/_ssr/{auth-rpc-BrJ6obHJ.mjs → auth-rpc-DUaeOWKK.mjs} +4 -4
- package/.output/server/_ssr/{auth.server-DwKoZoQM.mjs → auth.server-DW71WzsD.mjs} +2 -5
- package/.output/server/_ssr/{bundle-signing-rpc-COoQhW8E.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
- package/.output/server/_ssr/{alert-Dim3MGY5.mjs → card-BRiPmi0C.mjs} +61 -39
- package/.output/server/_ssr/{config.server-DEiLhKmy.mjs → config.server-afDuAwaG.mjs} +15 -8
- package/.output/server/_ssr/{createSsrRpc-9P0S0lJ3.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
- package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
- package/.output/server/_ssr/{databaseRows-CwbNwYSl.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
- package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
- package/.output/server/_ssr/{dialog-CQuRMNZ4.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-C-KUBfOi.mjs → router-B9wG_9f8.mjs} +37 -46
- package/.output/server/_ssr/{routes-Z7TwC6nC.mjs → routes-CKrrS2q2.mjs} +3 -3
- package/.output/server/_ssr/{routes-BM4ht9jb.mjs → routes-D7Yh_jfj.mjs} +402 -390
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
- package/.output/server/_ssr/{server-BjzmvXnO.mjs → server-D_C2rMzf.mjs} +5 -5
- package/.output/server/_ssr/{signing-k8GxyBDF.mjs → signing-Dz1WkZOf.mjs} +16 -85
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/{field-DRIXKjZG.mjs → table-CnZmon_Z.mjs} +51 -5
- package/.output/server/_ssr/{uuidv7-BbJ-9D_I.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
- package/.output/server/index.mjs +168 -203
- package/lib/index.d.mts +4 -4
- package/lib/vite-internal.mjs +1 -1
- package/lib/vite.d.mts +0 -1
- package/package.json +8 -8
- package/src/components/AppSidebar.tsx +12 -16
- package/src/components/ConsoleAccessPage.tsx +1 -1
- package/src/components/HashValueDisplay.tsx +11 -2
- package/src/components/features/api-keys/ApiKeysPage.tsx +1 -1
- 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 +121 -0
- package/src/components/features/{analytics/AnalyticsErrorAlert.tsx → insights/InsightsErrorAlert.tsx} +6 -6
- package/src/components/features/insights/InsightsOverview.tsx +203 -0
- 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 +250 -0
- package/src/components/features/insights/InstallationMatchesCard.tsx +178 -0
- package/src/components/features/insights/InstallationPageHeader.tsx +90 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
- package/src/components/features/signing/BundleSigningPage.tsx +18 -126
- 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 -3
- package/src/lib/api-rpc.ts +19 -90
- package/src/lib/api.ts +0 -99
- package/src/lib/insights-api.ts +100 -0
- package/src/lib/insights-rpc.ts +88 -0
- package/src/lib/insights-view.ts +15 -0
- package/src/lib/public-bundle-error.ts +19 -0
- package/src/lib/server/bundle-signing.server.ts +5 -82
- package/src/lib/server/console-runtime.server.ts +0 -9
- package/src/lib/server/releaseReachability.ts +1 -4
- package/src/lib/server/runtime.server.ts +6 -142
- package/src/routeTree.gen.ts +21 -21
- package/src/router.tsx +3 -0
- package/src/routes/-installations-search.ts +25 -12
- package/src/routes/__root.tsx +6 -16
- package/src/routes/api-keys.tsx +1 -1
- package/src/routes/index.tsx +21 -81
- package/src/routes/insights.tsx +84 -0
- package/src/routes/installations.tsx +294 -142
- package/src/styles.css +6 -0
- package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +0 -1
- package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +0 -42
- package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +0 -1
- package/.output/public/assets/alert-C-1KvDW8.js +0 -1
- package/.output/public/assets/analytics-Bqz0c-BT.js +0 -1
- package/.output/public/assets/analytics-api-CJdrTLLO.js +0 -1
- package/.output/public/assets/api-B_Rh-uzC.js +0 -1
- package/.output/public/assets/api-keys-BurCFhzH.js +0 -1
- package/.output/public/assets/api-keys-api-BEDJdQg3.js +0 -1
- package/.output/public/assets/badge-Bf0fZ-Gx.js +0 -1
- package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
- package/.output/public/assets/dialog-BS60BRr2.js +0 -1
- package/.output/public/assets/download-O-yNBc9H.js +0 -1
- package/.output/public/assets/field-Buq148-A.js +0 -1
- package/.output/public/assets/index-kFhd2S78.js +0 -10
- package/.output/public/assets/installations-BrQ-od41.js +0 -2
- package/.output/public/assets/installations-CGlZ3ROz.js +0 -1
- package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +0 -1
- package/.output/public/assets/loader-circle-DCYF-wJl.js +0 -1
- package/.output/public/assets/refresh-cw-n8XYbPPj.js +0 -1
- package/.output/public/assets/routes-CrdokJ80.js +0 -1
- package/.output/public/assets/search-DLhbWw3W.js +0 -1
- package/.output/public/assets/signing-HRexfjsH.js +0 -1
- package/.output/public/assets/styles-eB4EtvFS.css +0 -2
- package/.output/public/assets/table-D8CLieHX.js +0 -1
- package/.output/public/assets/textarea-YJLTVAoq.js +0 -1
- package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
- package/.output/public/assets/useOpenInteractionType-4cRD5xO0.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/AnalyticsCapabilityContext-ui4DX8GO.mjs +0 -15
- package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +0 -288
- package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +0 -13
- package/.output/server/_ssr/analytics-DtMTn8fn.mjs +0 -858
- package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +0 -71
- package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +0 -96
- package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +0 -155
- package/.output/server/_ssr/api-BTSv1tZb.mjs +0 -282
- package/.output/server/_ssr/badge-DCxbEeof.mjs +0 -26
- package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +0 -51
- package/.output/server/_ssr/installations-B-azgAvX.mjs +0 -19
- package/.output/server/_ssr/installations-Ctvzp2JH.mjs +0 -535
- package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +0 -527
- package/.output/server/_ssr/table-v1yCvYo7.mjs +0 -53
- package/.output/server/_ssr/textarea-DYHnskhb.mjs +0 -14
- package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +0 -108
- package/src/components/features/analytics/ActivityChart.tsx +0 -150
- package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +0 -24
- package/src/components/features/analytics/AnalyticsControls.tsx +0 -115
- package/src/components/features/analytics/AnalyticsOverview.tsx +0 -208
- package/src/components/features/analytics/BundleDistribution.tsx +0 -133
- package/src/components/features/analytics/BundleSelector.tsx +0 -114
- package/src/components/features/analytics/InstallationHistoryCard.tsx +0 -248
- package/src/components/features/analytics/InstallationMatchesCard.tsx +0 -132
- package/src/components/features/analytics/InstallationPageHeader.tsx +0 -138
- package/src/components/features/analytics/InstallationPagination.tsx +0 -48
- package/src/components/features/analytics/InstallationSearch.tsx +0 -230
- package/src/components/features/analytics/UpdateOutcomes.tsx +0 -147
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
- package/src/components/features/bundles/BundleAnalyticsSummary.tsx +0 -123
- package/src/lib/analytics-api.ts +0 -145
- package/src/lib/analytics-error.ts +0 -19
- package/src/lib/analytics-input.ts +0 -187
- package/src/lib/analytics-overview.ts +0 -170
- package/src/lib/analytics-rpc.ts +0 -218
- package/src/lib/server/releaseActivity.ts +0 -27
- package/src/routes/analytics.tsx +0 -139
- /package/.output/public/assets/{preload-helper-TKOBYGYD.js → preload-helper-CQ36nxok.js} +0 -0
|
@@ -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 { useAnalyticsCapability } from "@/components/features/analytics/AnalyticsCapabilityContext";
|
|
2
|
-
import { AnalyticsErrorAlert } from "@/components/features/analytics/AnalyticsErrorAlert";
|
|
3
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
5
|
-
import { useBundleEventAnalyticsQuery } from "@/lib/api";
|
|
6
|
-
|
|
7
|
-
import { BundleActivityChart } from "./BundleActivityChart";
|
|
8
|
-
|
|
9
|
-
interface BundleAnalyticsSummaryProps {
|
|
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 SupportedBundleAnalyticsSummary({
|
|
43
|
-
bundleId,
|
|
44
|
-
}: BundleAnalyticsSummaryProps) {
|
|
45
|
-
const { data, error, isLoading } = useBundleEventAnalyticsQuery(
|
|
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
|
-
<AnalyticsErrorAlert
|
|
76
|
-
error={
|
|
77
|
-
error instanceof Error
|
|
78
|
-
? error
|
|
79
|
-
: new Error("Failed to load bundle analytics.")
|
|
80
|
-
}
|
|
81
|
-
fallbackTitle="Analytics 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 BundleAnalyticsSummary({
|
|
114
|
-
bundleId,
|
|
115
|
-
}: BundleAnalyticsSummaryProps) {
|
|
116
|
-
const capability = useAnalyticsCapability();
|
|
117
|
-
|
|
118
|
-
if (capability.status !== "supported") {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return <SupportedBundleAnalyticsSummary bundleId={bundleId} />;
|
|
123
|
-
}
|
package/src/lib/analytics-api.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { type QueryClient, useQuery } from "@tanstack/react-query";
|
|
2
|
-
import { redirect } from "@tanstack/react-router";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
type ActiveInstallationInput,
|
|
6
|
-
parseActiveInstallationInput,
|
|
7
|
-
} from "./analytics-input";
|
|
8
|
-
import {
|
|
9
|
-
getActiveInstallationOverviewRpc,
|
|
10
|
-
getAnalyticsCapabilitiesRpc,
|
|
11
|
-
getAnalyticsOverviewRpc,
|
|
12
|
-
type AnalyticsCapabilities,
|
|
13
|
-
} from "./analytics-rpc";
|
|
14
|
-
|
|
15
|
-
export type AnalyticsCapabilityState =
|
|
16
|
-
| { readonly status: "unresolved" }
|
|
17
|
-
| { readonly status: "unsupported" }
|
|
18
|
-
| {
|
|
19
|
-
readonly status: "supported";
|
|
20
|
-
readonly mode: "bounded";
|
|
21
|
-
readonly maxMatchingRows: number;
|
|
22
|
-
}
|
|
23
|
-
| { readonly status: "error"; readonly error: Error };
|
|
24
|
-
|
|
25
|
-
type AnalyticsCapabilityQueryResult =
|
|
26
|
-
| { readonly status: "pending" }
|
|
27
|
-
| { readonly status: "success"; readonly data: AnalyticsCapabilities }
|
|
28
|
-
| { readonly status: "error"; readonly error: Error };
|
|
29
|
-
|
|
30
|
-
export type ProtectedAnalyticsRouteDecision =
|
|
31
|
-
| "loading"
|
|
32
|
-
| "redirect"
|
|
33
|
-
| "allow"
|
|
34
|
-
| "error";
|
|
35
|
-
|
|
36
|
-
export const analyticsQueryKeys = {
|
|
37
|
-
capabilities: ["analytics", "capabilities"] as const,
|
|
38
|
-
overview: ["analytics", "overview"] as const,
|
|
39
|
-
activeInstallations: (input: ActiveInstallationInput) =>
|
|
40
|
-
[
|
|
41
|
-
"analytics",
|
|
42
|
-
"active-installations",
|
|
43
|
-
input.window,
|
|
44
|
-
input.userId ?? null,
|
|
45
|
-
] as const,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const ANALYTICS_STALE_TIME_MS = 30_000;
|
|
49
|
-
|
|
50
|
-
export const getAnalyticsCapabilityState = (
|
|
51
|
-
query: AnalyticsCapabilityQueryResult,
|
|
52
|
-
): AnalyticsCapabilityState => {
|
|
53
|
-
switch (query.status) {
|
|
54
|
-
case "pending":
|
|
55
|
-
return { status: "unresolved" };
|
|
56
|
-
case "error":
|
|
57
|
-
return { status: "error", error: query.error };
|
|
58
|
-
case "success":
|
|
59
|
-
if (!query.data.capabilities.analytics) {
|
|
60
|
-
return { status: "unsupported" };
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
status: "supported",
|
|
64
|
-
mode: "bounded",
|
|
65
|
-
maxMatchingRows: query.data.capabilities.maxMatchingRows,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const getProtectedAnalyticsRouteDecision = (
|
|
71
|
-
capability: AnalyticsCapabilityState,
|
|
72
|
-
): ProtectedAnalyticsRouteDecision => {
|
|
73
|
-
switch (capability.status) {
|
|
74
|
-
case "unresolved":
|
|
75
|
-
return "loading";
|
|
76
|
-
case "unsupported":
|
|
77
|
-
return "redirect";
|
|
78
|
-
case "supported":
|
|
79
|
-
return "allow";
|
|
80
|
-
case "error":
|
|
81
|
-
return "error";
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export const isAnalyticsQueryEnabled = (
|
|
86
|
-
capability: AnalyticsCapabilityState,
|
|
87
|
-
): boolean => capability.status === "supported";
|
|
88
|
-
|
|
89
|
-
export const getAnalyticsCapabilitiesQueryOptions = () => ({
|
|
90
|
-
queryKey: analyticsQueryKeys.capabilities,
|
|
91
|
-
queryFn: () => getAnalyticsCapabilitiesRpc(),
|
|
92
|
-
staleTime: Infinity,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
export const useAnalyticsCapabilitiesQuery = () =>
|
|
96
|
-
useQuery(getAnalyticsCapabilitiesQueryOptions());
|
|
97
|
-
|
|
98
|
-
export const ensureAnalyticsRouteAccess = async (
|
|
99
|
-
queryClient: QueryClient,
|
|
100
|
-
): Promise<void> => {
|
|
101
|
-
const result = await queryClient.ensureQueryData(
|
|
102
|
-
getAnalyticsCapabilitiesQueryOptions(),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
if (!result.capabilities.analytics) {
|
|
106
|
-
throw redirect({
|
|
107
|
-
to: "/",
|
|
108
|
-
search: {},
|
|
109
|
-
replace: true,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export const getAnalyticsOverviewQueryOptions = (
|
|
115
|
-
capability: AnalyticsCapabilityState,
|
|
116
|
-
) => ({
|
|
117
|
-
queryKey: analyticsQueryKeys.overview,
|
|
118
|
-
queryFn: () => getAnalyticsOverviewRpc(),
|
|
119
|
-
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
120
|
-
refetchOnWindowFocus: true,
|
|
121
|
-
enabled: isAnalyticsQueryEnabled(capability),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
export const useAnalyticsOverviewQuery = (
|
|
125
|
-
capability: AnalyticsCapabilityState,
|
|
126
|
-
) => useQuery(getAnalyticsOverviewQueryOptions(capability));
|
|
127
|
-
|
|
128
|
-
export const getActiveInstallationQueryOptions = (
|
|
129
|
-
capability: AnalyticsCapabilityState,
|
|
130
|
-
input: ActiveInstallationInput,
|
|
131
|
-
) => {
|
|
132
|
-
const normalized = parseActiveInstallationInput(input);
|
|
133
|
-
return {
|
|
134
|
-
queryKey: analyticsQueryKeys.activeInstallations(normalized),
|
|
135
|
-
queryFn: () => getActiveInstallationOverviewRpc({ data: normalized }),
|
|
136
|
-
staleTime: ANALYTICS_STALE_TIME_MS,
|
|
137
|
-
refetchOnWindowFocus: true,
|
|
138
|
-
enabled: isAnalyticsQueryEnabled(capability),
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
export const useActiveInstallationQuery = (
|
|
143
|
-
capability: AnalyticsCapabilityState,
|
|
144
|
-
input: ActiveInstallationInput,
|
|
145
|
-
) => useQuery(getActiveInstallationQueryOptions(capability, input));
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type AnalyticsErrorCopy = {
|
|
2
|
-
readonly title: string;
|
|
3
|
-
readonly description: string;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export const getAnalyticsErrorCopy = (
|
|
7
|
-
error: Error,
|
|
8
|
-
fallbackTitle: string,
|
|
9
|
-
): AnalyticsErrorCopy => {
|
|
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: "Analytics report limit reached",
|
|
17
|
-
description: `This query matched more than ${limit.toLocaleString()} reports. Narrow the query and try again.`,
|
|
18
|
-
};
|
|
19
|
-
};
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import type { ActiveInstallationWindow } from "@hot-updater/server";
|
|
2
|
-
import type { BundleEventAnalyticsWindow } from "@hot-updater/server";
|
|
3
|
-
|
|
4
|
-
const MAX_ANALYTICS_STRING_LENGTH = 1024;
|
|
5
|
-
const MAX_ANALYTICS_LIMIT = 100;
|
|
6
|
-
|
|
7
|
-
export type BundleEventSummaryInput = {
|
|
8
|
-
readonly bundleId: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type BundleEventAnalyticsInput = BundleEventSummaryInput & {
|
|
12
|
-
readonly window: BundleEventAnalyticsWindow;
|
|
13
|
-
readonly limit?: number;
|
|
14
|
-
readonly offset?: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type SearchInstallationsInput = {
|
|
18
|
-
readonly query: string;
|
|
19
|
-
readonly limit?: number;
|
|
20
|
-
readonly offset?: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type InstallationHistoryInput = {
|
|
24
|
-
readonly installId: string;
|
|
25
|
-
readonly limit?: number;
|
|
26
|
-
readonly offset?: number;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type ActiveInstallationInput = {
|
|
30
|
-
readonly window: ActiveInstallationWindow;
|
|
31
|
-
readonly userId?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type AnalyticsPagination = {
|
|
35
|
-
readonly limit?: number;
|
|
36
|
-
readonly offset?: number;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export class AnalyticsInputValidationError extends Error {
|
|
40
|
-
readonly name = "AnalyticsInputValidationError";
|
|
41
|
-
|
|
42
|
-
constructor(readonly field: string) {
|
|
43
|
-
super(`Invalid analytics input: ${field}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const isRecord = (input: unknown): input is Record<string, unknown> =>
|
|
48
|
-
typeof input === "object" && input !== null && !Array.isArray(input);
|
|
49
|
-
|
|
50
|
-
const parseRecord = (input: unknown): Record<string, unknown> => {
|
|
51
|
-
if (!isRecord(input)) {
|
|
52
|
-
throw new AnalyticsInputValidationError("input");
|
|
53
|
-
}
|
|
54
|
-
return input;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const parseString = (input: Record<string, unknown>, field: string): string => {
|
|
58
|
-
const value = input[field];
|
|
59
|
-
if (typeof value !== "string") {
|
|
60
|
-
throw new AnalyticsInputValidationError(field);
|
|
61
|
-
}
|
|
62
|
-
const trimmed = value.trim();
|
|
63
|
-
if (trimmed.length === 0 || trimmed.length > MAX_ANALYTICS_STRING_LENGTH) {
|
|
64
|
-
throw new AnalyticsInputValidationError(field);
|
|
65
|
-
}
|
|
66
|
-
return trimmed;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const parseLimit = (input: Record<string, unknown>): number | undefined => {
|
|
70
|
-
const value = input.limit;
|
|
71
|
-
if (value === undefined) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
if (
|
|
75
|
-
typeof value !== "number" ||
|
|
76
|
-
!Number.isInteger(value) ||
|
|
77
|
-
value < 1 ||
|
|
78
|
-
value > MAX_ANALYTICS_LIMIT
|
|
79
|
-
) {
|
|
80
|
-
throw new AnalyticsInputValidationError("limit");
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const parseOffset = (input: Record<string, unknown>): number | undefined => {
|
|
86
|
-
const value = input.offset;
|
|
87
|
-
if (value === undefined) {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
91
|
-
throw new AnalyticsInputValidationError("offset");
|
|
92
|
-
}
|
|
93
|
-
return value;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const parsePagination = (
|
|
97
|
-
input: Record<string, unknown>,
|
|
98
|
-
): AnalyticsPagination => {
|
|
99
|
-
const limit = parseLimit(input);
|
|
100
|
-
const offset = parseOffset(input);
|
|
101
|
-
return {
|
|
102
|
-
...(limit === undefined ? {} : { limit }),
|
|
103
|
-
...(offset === undefined ? {} : { offset }),
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const parseWindow = (
|
|
108
|
-
input: Record<string, unknown>,
|
|
109
|
-
): BundleEventAnalyticsWindow => {
|
|
110
|
-
const value = input.window;
|
|
111
|
-
if (value === "24h" || value === "7d" || value === "30d" || value === "all") {
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
throw new AnalyticsInputValidationError("window");
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const parseActiveWindow = (
|
|
118
|
-
input: Record<string, unknown>,
|
|
119
|
-
): ActiveInstallationWindow => {
|
|
120
|
-
const value = input.window;
|
|
121
|
-
if (value === "24h" || value === "7d" || value === "30d") {
|
|
122
|
-
return value;
|
|
123
|
-
}
|
|
124
|
-
throw new AnalyticsInputValidationError("window");
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const parseOptionalUserId = (
|
|
128
|
-
input: Record<string, unknown>,
|
|
129
|
-
): string | undefined => {
|
|
130
|
-
if (input.userId === undefined) return undefined;
|
|
131
|
-
if (typeof input.userId !== "string") {
|
|
132
|
-
throw new AnalyticsInputValidationError("userId");
|
|
133
|
-
}
|
|
134
|
-
const userId = input.userId.trim();
|
|
135
|
-
if (userId.length === 0) return undefined;
|
|
136
|
-
if (userId.length > MAX_ANALYTICS_STRING_LENGTH) {
|
|
137
|
-
throw new AnalyticsInputValidationError("userId");
|
|
138
|
-
}
|
|
139
|
-
return userId;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
export const parseActiveInstallationInput = (
|
|
143
|
-
input: unknown,
|
|
144
|
-
): ActiveInstallationInput => {
|
|
145
|
-
const record = parseRecord(input);
|
|
146
|
-
const window = parseActiveWindow(record);
|
|
147
|
-
const userId = parseOptionalUserId(record);
|
|
148
|
-
return userId === undefined ? { window } : { window, userId };
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const parseBundleEventSummaryInput = (
|
|
152
|
-
input: unknown,
|
|
153
|
-
): BundleEventSummaryInput => {
|
|
154
|
-
const record = parseRecord(input);
|
|
155
|
-
return { bundleId: parseString(record, "bundleId") };
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export const parseBundleEventAnalyticsInput = (
|
|
159
|
-
input: unknown,
|
|
160
|
-
): BundleEventAnalyticsInput => {
|
|
161
|
-
const record = parseRecord(input);
|
|
162
|
-
return {
|
|
163
|
-
bundleId: parseString(record, "bundleId"),
|
|
164
|
-
window: parseWindow(record),
|
|
165
|
-
...parsePagination(record),
|
|
166
|
-
};
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const parseSearchInstallationsInput = (
|
|
170
|
-
input: unknown,
|
|
171
|
-
): SearchInstallationsInput => {
|
|
172
|
-
const record = parseRecord(input);
|
|
173
|
-
return {
|
|
174
|
-
query: parseString(record, "query"),
|
|
175
|
-
...parsePagination(record),
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
export const parseInstallationHistoryInput = (
|
|
180
|
-
input: unknown,
|
|
181
|
-
): InstallationHistoryInput => {
|
|
182
|
-
const record = parseRecord(input);
|
|
183
|
-
return {
|
|
184
|
-
installId: parseString(record, "installId"),
|
|
185
|
-
...parsePagination(record),
|
|
186
|
-
};
|
|
187
|
-
};
|