@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { InsightsBundleSelection } from "@hot-updater/server";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { useInsightsEventsQuery } from "@/lib/insights-api";
|
|
5
|
+
import { outcomeLabels } from "@/lib/insights-view";
|
|
6
|
+
|
|
7
|
+
import { EventHistoryCard } from "./EventHistoryCard";
|
|
8
|
+
|
|
9
|
+
export type InsightsReportSelection = {
|
|
10
|
+
readonly bundle: InsightsBundleSelection;
|
|
11
|
+
readonly sinceMs: number;
|
|
12
|
+
readonly beforeReceivedAtMs: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function InsightsBundleReports({
|
|
16
|
+
selection,
|
|
17
|
+
}: {
|
|
18
|
+
readonly selection: InsightsReportSelection;
|
|
19
|
+
}) {
|
|
20
|
+
const [cursors, setCursors] = useState<readonly (string | undefined)[]>([
|
|
21
|
+
undefined,
|
|
22
|
+
]);
|
|
23
|
+
const events = useInsightsEventsQuery(
|
|
24
|
+
{ ...selection, cursor: cursors.at(-1), limit: 50 },
|
|
25
|
+
true,
|
|
26
|
+
);
|
|
27
|
+
return (
|
|
28
|
+
<EventHistoryCard
|
|
29
|
+
title={outcomeLabels[selection.bundle.outcome]}
|
|
30
|
+
error={events.error}
|
|
31
|
+
eventsLocation={{ eventsBefore: selection.beforeReceivedAtMs }}
|
|
32
|
+
history={events.data}
|
|
33
|
+
isFetching={events.isFetching}
|
|
34
|
+
isLoading={events.isLoading}
|
|
35
|
+
onNext={() => {
|
|
36
|
+
const cursor = events.data?.nextCursor;
|
|
37
|
+
if (cursor) setCursors((previous) => [...previous, cursor]);
|
|
38
|
+
}}
|
|
39
|
+
onPrevious={() => setCursors((previous) => previous.slice(0, -1))}
|
|
40
|
+
onRefresh={() => {
|
|
41
|
+
void events.refetch();
|
|
42
|
+
}}
|
|
43
|
+
pageNumber={cursors.length}
|
|
44
|
+
>
|
|
45
|
+
<p className="text-sm text-muted-foreground">
|
|
46
|
+
Reports for the selected bundle and the measured period. New reports
|
|
47
|
+
appear after refreshing the overview.
|
|
48
|
+
</p>
|
|
49
|
+
</EventHistoryCard>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
7
|
+
import type { InsightsOverviewInput, InsightsWindow } from "@/lib/insights-api";
|
|
8
|
+
|
|
9
|
+
const windows = [
|
|
10
|
+
{ value: "24h", label: "24 hours", shortLabel: "24h" },
|
|
11
|
+
{ value: "7d", label: "7 days", shortLabel: "7d" },
|
|
12
|
+
{ value: "30d", label: "30 days", shortLabel: "30d" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export function InsightsControls({
|
|
16
|
+
onWindowChange,
|
|
17
|
+
onScopeChange,
|
|
18
|
+
scope,
|
|
19
|
+
window,
|
|
20
|
+
}: {
|
|
21
|
+
readonly onWindowChange: (window: InsightsWindow) => void;
|
|
22
|
+
readonly onScopeChange: (
|
|
23
|
+
scope: Omit<InsightsOverviewInput, "window">,
|
|
24
|
+
) => void;
|
|
25
|
+
readonly scope: Omit<InsightsOverviewInput, "window">;
|
|
26
|
+
readonly window: InsightsWindow;
|
|
27
|
+
}) {
|
|
28
|
+
const [platform, setPlatform] = useState(scope.platform);
|
|
29
|
+
const [channel, setChannel] = useState(scope.channel);
|
|
30
|
+
const [bundleId, setBundleId] = useState(scope.bundleId ?? "");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section aria-label="Insights controls" className="flex flex-col gap-4">
|
|
34
|
+
<form
|
|
35
|
+
onSubmit={(event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
onScopeChange({ platform, channel, bundleId: bundleId || undefined });
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<FieldGroup className="items-end sm:flex-row">
|
|
41
|
+
<Field className="w-full sm:w-auto">
|
|
42
|
+
<FieldLabel>Platform</FieldLabel>
|
|
43
|
+
<ToggleGroup
|
|
44
|
+
aria-label="Platform"
|
|
45
|
+
value={[platform]}
|
|
46
|
+
onValueChange={(value) => {
|
|
47
|
+
if (value[0] === "ios" || value[0] === "android")
|
|
48
|
+
setPlatform(value[0]);
|
|
49
|
+
}}
|
|
50
|
+
spacing={0}
|
|
51
|
+
variant="outline"
|
|
52
|
+
>
|
|
53
|
+
<ToggleGroupItem className="h-11 lg:h-8" value="ios">
|
|
54
|
+
iOS
|
|
55
|
+
</ToggleGroupItem>
|
|
56
|
+
<ToggleGroupItem className="h-11 lg:h-8" value="android">
|
|
57
|
+
Android
|
|
58
|
+
</ToggleGroupItem>
|
|
59
|
+
</ToggleGroup>
|
|
60
|
+
</Field>
|
|
61
|
+
<Field>
|
|
62
|
+
<FieldLabel htmlFor="insights-channel">Channel</FieldLabel>
|
|
63
|
+
<Input
|
|
64
|
+
className="h-11 lg:h-8"
|
|
65
|
+
id="insights-channel"
|
|
66
|
+
value={channel}
|
|
67
|
+
onChange={(event) => setChannel(event.target.value)}
|
|
68
|
+
required
|
|
69
|
+
maxLength={1024}
|
|
70
|
+
/>
|
|
71
|
+
</Field>
|
|
72
|
+
<Field>
|
|
73
|
+
<FieldLabel htmlFor="insights-bundle">
|
|
74
|
+
Bundle ID (optional)
|
|
75
|
+
</FieldLabel>
|
|
76
|
+
<Input
|
|
77
|
+
className="h-11 lg:h-8"
|
|
78
|
+
id="insights-bundle"
|
|
79
|
+
placeholder="Select a bundle by ID"
|
|
80
|
+
value={bundleId}
|
|
81
|
+
onChange={(event) => setBundleId(event.target.value)}
|
|
82
|
+
maxLength={1024}
|
|
83
|
+
/>
|
|
84
|
+
</Field>
|
|
85
|
+
<Button
|
|
86
|
+
className="h-11 w-full sm:w-auto lg:h-8"
|
|
87
|
+
type="submit"
|
|
88
|
+
variant="outline"
|
|
89
|
+
>
|
|
90
|
+
Apply filters
|
|
91
|
+
</Button>
|
|
92
|
+
</FieldGroup>
|
|
93
|
+
</form>
|
|
94
|
+
<Field orientation="horizontal" className="w-full">
|
|
95
|
+
<FieldLabel className="sr-only">Reporting period</FieldLabel>
|
|
96
|
+
<ToggleGroup
|
|
97
|
+
aria-label="Reporting period"
|
|
98
|
+
className="w-full sm:w-fit"
|
|
99
|
+
onValueChange={(value) => {
|
|
100
|
+
if (value[0]) onWindowChange(value[0] as InsightsWindow);
|
|
101
|
+
}}
|
|
102
|
+
spacing={0}
|
|
103
|
+
size="lg"
|
|
104
|
+
value={[window]}
|
|
105
|
+
variant="outline"
|
|
106
|
+
>
|
|
107
|
+
{windows.map((item) => (
|
|
108
|
+
<ToggleGroupItem
|
|
109
|
+
aria-label={item.label}
|
|
110
|
+
className="h-11 flex-1 px-4 sm:flex-none lg:h-8 lg:px-2.5"
|
|
111
|
+
key={item.value}
|
|
112
|
+
value={item.value}
|
|
113
|
+
>
|
|
114
|
+
{item.shortLabel}
|
|
115
|
+
</ToggleGroupItem>
|
|
116
|
+
))}
|
|
117
|
+
</ToggleGroup>
|
|
118
|
+
</Field>
|
|
119
|
+
</section>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { TriangleAlert } from "lucide-react";
|
|
2
2
|
|
|
3
3
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
4
|
-
import { getAnalyticsErrorCopy } from "@/lib/analytics-error";
|
|
5
4
|
|
|
6
|
-
export function
|
|
7
|
-
error,
|
|
5
|
+
export function InsightsErrorAlert({
|
|
8
6
|
fallbackTitle,
|
|
9
7
|
}: {
|
|
10
8
|
readonly error: Error;
|
|
11
9
|
readonly fallbackTitle: string;
|
|
12
10
|
}) {
|
|
13
|
-
const copy = getAnalyticsErrorCopy(error, fallbackTitle);
|
|
14
11
|
return (
|
|
15
12
|
<Alert variant="destructive">
|
|
16
13
|
<TriangleAlert aria-hidden="true" />
|
|
17
|
-
<AlertTitle>{
|
|
18
|
-
<AlertDescription>
|
|
14
|
+
<AlertTitle>{fallbackTitle}</AlertTitle>
|
|
15
|
+
<AlertDescription>
|
|
16
|
+
Refresh to try again. If this keeps happening, check your Insights
|
|
17
|
+
provider connection.
|
|
18
|
+
</AlertDescription>
|
|
19
19
|
</Alert>
|
|
20
20
|
);
|
|
21
21
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { ArrowUpRight } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardFooter,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "@/components/ui/card";
|
|
13
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
14
|
+
import type { ReportingInstallations } from "@/lib/insights-api";
|
|
15
|
+
import { outcomeLabels } from "@/lib/insights-view";
|
|
16
|
+
|
|
17
|
+
import { EventTimestamp, useInsightsTimeFormat } from "./EventDetails";
|
|
18
|
+
import type { InsightsReportSelection } from "./InsightsBundleReports";
|
|
19
|
+
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
20
|
+
|
|
21
|
+
type InsightsOverviewProps =
|
|
22
|
+
| { readonly status: "loading" }
|
|
23
|
+
| { readonly status: "error"; readonly error: Error }
|
|
24
|
+
| {
|
|
25
|
+
readonly status: "success";
|
|
26
|
+
readonly active: ReportingInstallations;
|
|
27
|
+
readonly onOutcomeSelect: (selection: InsightsReportSelection) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const windowCopy = {
|
|
31
|
+
"24h": "the last 24 hours",
|
|
32
|
+
"7d": "the last 7 days",
|
|
33
|
+
"30d": "the last 30 days",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export function InsightsOverview(props: InsightsOverviewProps) {
|
|
37
|
+
const dateTimeFormat = useInsightsTimeFormat();
|
|
38
|
+
|
|
39
|
+
if (props.status === "loading") {
|
|
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;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<section
|
|
70
|
+
aria-labelledby="reporting-installations-heading"
|
|
71
|
+
className="flex flex-col gap-4"
|
|
72
|
+
>
|
|
73
|
+
<Card className="min-w-0 overflow-hidden shadow-sm">
|
|
74
|
+
<CardHeader className="gap-2 px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
|
|
75
|
+
<CardTitle className="text-sm font-medium">
|
|
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()}
|
|
89
|
+
</p>
|
|
90
|
+
</CardContent>
|
|
91
|
+
<CardFooter className="flex flex-wrap items-center justify-between gap-3 border-t bg-muted/15 px-4 py-3 sm:px-6">
|
|
92
|
+
<dl className="min-w-0 text-xs">
|
|
93
|
+
<dt className="text-muted-foreground">Measured at</dt>
|
|
94
|
+
<dd className="font-medium tabular-nums">
|
|
95
|
+
<EventTimestamp
|
|
96
|
+
formatter={dateTimeFormat}
|
|
97
|
+
touch
|
|
98
|
+
value={active.reportingInstallations.measuredAtMs}
|
|
99
|
+
/>
|
|
100
|
+
</dd>
|
|
101
|
+
</dl>
|
|
102
|
+
<Link
|
|
103
|
+
className={buttonVariants({
|
|
104
|
+
className: "h-11 px-3 lg:h-8",
|
|
105
|
+
variant: "outline",
|
|
106
|
+
})}
|
|
107
|
+
to="/installations"
|
|
108
|
+
>
|
|
109
|
+
View events
|
|
110
|
+
<ArrowUpRight aria-hidden="true" data-icon="inline-end" />
|
|
111
|
+
</Link>
|
|
112
|
+
</CardFooter>
|
|
113
|
+
</Card>
|
|
114
|
+
{bundle ? (
|
|
115
|
+
<Card className="min-w-0 overflow-hidden shadow-sm">
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle>Selected bundle</CardTitle>
|
|
118
|
+
<CardDescription className="break-all">
|
|
119
|
+
{bundle.bundleId}
|
|
120
|
+
</CardDescription>
|
|
121
|
+
</CardHeader>
|
|
122
|
+
<CardContent>
|
|
123
|
+
<dl className="flex flex-col gap-5">
|
|
124
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
125
|
+
<div>
|
|
126
|
+
<dt className="text-sm">Selected bundle installations</dt>
|
|
127
|
+
<dd className="mt-1 text-2xl font-semibold tabular-nums">
|
|
128
|
+
{bundle.reportingInstallations.count.toLocaleString()}
|
|
129
|
+
</dd>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="text-xs text-muted-foreground">
|
|
132
|
+
Measured at{" "}
|
|
133
|
+
<EventTimestamp
|
|
134
|
+
formatter={dateTimeFormat}
|
|
135
|
+
touch
|
|
136
|
+
value={bundle.reportingInstallations.measuredAtMs}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{(
|
|
141
|
+
[
|
|
142
|
+
["applied", bundle.appliedReports],
|
|
143
|
+
["recovered", bundle.recoveredReports],
|
|
144
|
+
["adopted", bundle.adoptedReports],
|
|
145
|
+
] as const
|
|
146
|
+
).map(([outcome, measurement]) => (
|
|
147
|
+
<div
|
|
148
|
+
className="flex flex-wrap items-center justify-between gap-3"
|
|
149
|
+
key={outcome}
|
|
150
|
+
>
|
|
151
|
+
<div>
|
|
152
|
+
<dt className="text-sm">{outcomeLabels[outcome]}</dt>
|
|
153
|
+
<dd className="mt-1 text-2xl font-semibold tabular-nums">
|
|
154
|
+
{measurement.count.toLocaleString()}
|
|
155
|
+
</dd>
|
|
156
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
157
|
+
Measured at{" "}
|
|
158
|
+
<EventTimestamp
|
|
159
|
+
formatter={dateTimeFormat}
|
|
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
|
+
})
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
View {outcome} reports
|
|
182
|
+
<ArrowUpRight aria-hidden="true" data-icon="inline-end" />
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</dl>
|
|
187
|
+
</CardContent>
|
|
188
|
+
<CardFooter>
|
|
189
|
+
<p className="text-xs text-muted-foreground">
|
|
190
|
+
Installation counts are independent live measurements. Outcome
|
|
191
|
+
counts are accepted reports, not unique devices or update
|
|
192
|
+
attempts. Recovery reports identify the bundle recovered from.
|
|
193
|
+
</p>
|
|
194
|
+
</CardFooter>
|
|
195
|
+
</Card>
|
|
196
|
+
) : null}
|
|
197
|
+
<p className="text-xs text-muted-foreground">
|
|
198
|
+
Offline installations, opt-outs, and reports that did not reach the
|
|
199
|
+
server are absent.
|
|
200
|
+
</p>
|
|
201
|
+
</section>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { ChartNoAxesCombined } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
5
|
+
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
6
|
+
|
|
7
|
+
export function InsightsPageHeader({
|
|
8
|
+
view,
|
|
9
|
+
}: {
|
|
10
|
+
readonly view: "overview" | "events";
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-3 border-b bg-background px-3 py-3 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
|
|
14
|
+
<SidebarTrigger className="-ml-1 size-11 lg:size-7" />
|
|
15
|
+
<div className="flex items-center gap-1.5">
|
|
16
|
+
<ChartNoAxesCombined
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
className="size-3.5 text-muted-foreground"
|
|
19
|
+
/>
|
|
20
|
+
<h1 className="text-sm font-medium">Insights</h1>
|
|
21
|
+
</div>
|
|
22
|
+
<nav aria-label="Insights views" className="ml-auto flex gap-1">
|
|
23
|
+
<Link
|
|
24
|
+
aria-current={view === "overview" ? "page" : undefined}
|
|
25
|
+
className={buttonVariants({
|
|
26
|
+
className: "h-11 px-3 lg:h-8 lg:px-2.5",
|
|
27
|
+
size: "lg",
|
|
28
|
+
variant: view === "overview" ? "secondary" : "ghost",
|
|
29
|
+
})}
|
|
30
|
+
to="/insights"
|
|
31
|
+
>
|
|
32
|
+
Overview
|
|
33
|
+
</Link>
|
|
34
|
+
<Link
|
|
35
|
+
aria-current={view === "events" ? "page" : undefined}
|
|
36
|
+
className={buttonVariants({
|
|
37
|
+
className: "h-11 px-3 lg:h-8 lg:px-2.5",
|
|
38
|
+
size: "lg",
|
|
39
|
+
variant: view === "events" ? "secondary" : "ghost",
|
|
40
|
+
})}
|
|
41
|
+
to="/installations"
|
|
42
|
+
>
|
|
43
|
+
Events
|
|
44
|
+
</Link>
|
|
45
|
+
</nav>
|
|
46
|
+
</header>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
|
|
3
|
+
export function InsightsPagination({
|
|
4
|
+
hasPrevious,
|
|
5
|
+
label,
|
|
6
|
+
nextCursor,
|
|
7
|
+
onNext,
|
|
8
|
+
onPrevious,
|
|
9
|
+
pageLength,
|
|
10
|
+
pageNumber,
|
|
11
|
+
}: {
|
|
12
|
+
readonly hasPrevious: boolean;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly nextCursor: string | null;
|
|
15
|
+
readonly onNext: () => void;
|
|
16
|
+
readonly onPrevious: () => void;
|
|
17
|
+
readonly pageLength: number;
|
|
18
|
+
readonly pageNumber: number;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<nav
|
|
22
|
+
aria-label={`${label} pagination`}
|
|
23
|
+
className="flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3 sm:px-6"
|
|
24
|
+
>
|
|
25
|
+
<span className="text-xs text-muted-foreground tabular-nums">
|
|
26
|
+
Page {pageNumber} · {pageLength.toLocaleString()} rows
|
|
27
|
+
</span>
|
|
28
|
+
<div className="flex gap-2">
|
|
29
|
+
<Button
|
|
30
|
+
className="h-11 min-w-11 px-3 lg:h-8 lg:min-w-0 lg:px-2"
|
|
31
|
+
disabled={!hasPrevious}
|
|
32
|
+
onClick={onPrevious}
|
|
33
|
+
size="sm"
|
|
34
|
+
type="button"
|
|
35
|
+
variant="outline"
|
|
36
|
+
>
|
|
37
|
+
Previous
|
|
38
|
+
</Button>
|
|
39
|
+
<Button
|
|
40
|
+
className="h-11 min-w-11 px-3 lg:h-8 lg:min-w-0 lg:px-2"
|
|
41
|
+
disabled={nextCursor === null}
|
|
42
|
+
onClick={onNext}
|
|
43
|
+
size="sm"
|
|
44
|
+
type="button"
|
|
45
|
+
variant="outline"
|
|
46
|
+
>
|
|
47
|
+
Next
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
</nav>
|
|
51
|
+
);
|
|
52
|
+
}
|