@hot-updater/console 1.0.0-rc.3 → 1.0.0-rc.31
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/ConsoleFeatureUnavailable-BH7LLqAg.js +1 -0
- package/.output/public/assets/EventDetails-DZby1OQH.js +1 -0
- package/.output/public/assets/InsightsInfo-B8i-PPsH.js +1 -0
- package/.output/public/assets/InsightsPageHeader-DUgKPOEO.js +1 -0
- package/.output/public/assets/NotFoundPage-CkgdpWvl.js +1 -0
- package/.output/public/assets/alert-dialog-DXxrSU0G.js +1 -0
- package/.output/public/assets/api-BiZP_i-u.js +1 -0
- package/.output/public/assets/api-keys-DQDqmKtq.js +1 -0
- package/.output/public/assets/api-keys-DTQ1HrkR.js +1 -0
- package/.output/public/assets/arrow-left-BPK7iqvj.js +1 -0
- package/.output/public/assets/card-DKr2pZMA.js +1 -0
- package/.output/public/assets/console-features-api-BhUIxeL-.js +1 -0
- package/.output/public/assets/dialog-DxjO7ofz.js +1 -0
- package/.output/public/assets/dist-BzWwzt98.js +1 -0
- package/.output/public/assets/empty-DtangHOp.js +1 -0
- package/.output/public/assets/field-bPoDrs6K.js +1 -0
- package/.output/public/assets/index-CukOHJfR.js +11 -0
- package/.output/public/assets/input-group-_PD0i7wZ.js +1 -0
- package/.output/public/assets/insights-BBya_x8N.js +1 -0
- package/.output/public/assets/insights-BfxN6oJt.js +2 -0
- package/.output/public/assets/insights-DSo4OVJK.js +42 -0
- package/.output/public/assets/insights-usage-rpc-Ceygyqz5.js +1 -0
- package/.output/public/assets/insights_.distribution-BKOhL2-k.js +2 -0
- package/.output/public/assets/insights_.distribution-Qq4cisRR.js +1 -0
- package/.output/public/assets/insights_.distribution-WTcKITzb.js +1 -0
- package/.output/public/assets/installations-CstW4tEL.js +1 -0
- package/.output/public/assets/installations-DKMg-0wU.js +2 -0
- package/.output/public/assets/installations-_UEoKAn_.js +1 -0
- package/.output/public/assets/loader-circle-iTGehfyV.js +1 -0
- package/.output/public/assets/mutation-B4EZNS91.js +1 -0
- package/.output/public/assets/preload-helper-DuV6lz46.js +1 -0
- package/.output/public/assets/refresh-cw-3fSQ1FYY.js +1 -0
- package/.output/public/assets/routes-TFPucRmX.js +2 -0
- package/.output/public/assets/routes-_sHQ8uxu.js +1 -0
- package/.output/public/assets/sidebar-RBvHzq8e.js +3 -0
- package/.output/public/assets/styles-DmbQqD7a.css +2 -0
- package/.output/public/assets/table-BYmouPt9.js +1 -0
- package/.output/public/assets/useCompositeListItem-BxJspDxS.js +1 -0
- package/.output/public/assets/valueToPercent-BjsIMB74.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-EMel9KYs.mjs → __23tanstack-start-server-fn-resolver-B5vj_i3l.mjs} +63 -51
- package/.output/server/_libs/@base-ui/react+[...].mjs +6891 -3364
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
- package/.output/server/_libs/lucide-react.mjs +71 -31
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +1 -1
- package/.output/server/_libs/recharts+[...].mjs +1 -1
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__history.mjs +68 -41
- package/.output/server/_libs/tanstack__query-core.mjs +1994 -310
- package/.output/server/_libs/tanstack__react-query.mjs +448 -21
- package/.output/server/_libs/verkit.mjs +99 -1
- package/.output/server/_ssr/ConsoleFeatureUnavailable-BuWwEnk_.mjs +58 -0
- package/.output/server/_ssr/EventDetails-ksr9nh9K.mjs +289 -0
- package/.output/server/_ssr/InsightsInfo-C4VjjzZR.mjs +75 -0
- package/.output/server/_ssr/InsightsPageHeader-vt4sJWTO.mjs +161 -0
- package/.output/server/_ssr/NotFoundPage-B_NdWE3y.mjs +37 -0
- package/.output/server/_ssr/{dialog-DNtKYR5K.mjs → alert-dialog-CKVhOIS_.mjs} +6 -83
- package/.output/server/_ssr/api-gs0XqRQX.mjs +203 -0
- package/.output/server/_ssr/{api-keys-BBl84_lN.mjs → api-keys-BLS-VTWq.mjs} +49 -8
- package/.output/server/_ssr/api-keys-C8VVGWjc.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-xuz9tsHh.mjs +52 -0
- package/.output/server/_ssr/api-rpc-CCiLRUac.mjs +296 -0
- package/.output/server/_ssr/{auth-rpc-gaibR719.mjs → auth-rpc-CUFtJtD0.mjs} +6 -6
- package/.output/server/_ssr/auth.server-BUj7X-LJ.mjs +5712 -0
- package/.output/server/_ssr/bundleActivity-H7ccYo-o.mjs +18 -0
- package/.output/server/_ssr/card-CTElTN0R.mjs +49 -0
- package/.output/server/_ssr/config-nsIcsHh8.mjs +15 -0
- package/.output/server/_ssr/config.server-BEyzkj4G.mjs +36 -0
- package/.output/server/_ssr/console-access-Ys4Zkk_1.mjs +17 -0
- package/.output/server/_ssr/console-features-AGV2b_l5.mjs +55 -0
- package/.output/server/_ssr/console-features-api-BmJFrtS2.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-C8w1DJhN.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-BdBf_ezN.mjs} +67 -56
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-B_y2lrwA.mjs} +2 -2
- package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Bhxciy7Z.mjs} +28 -5
- package/.output/server/_ssr/databaseRows-DwMAjaF8.mjs +74 -0
- package/.output/server/_ssr/dialog-CTvSCO60.mjs +84 -0
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-Cxo9ghYv.mjs} +140 -6
- package/.output/server/_ssr/{downloadBundle-A9zYC2go.mjs → downloadBundle-D10vIrle.mjs} +10 -10
- package/.output/server/_ssr/empty-IW1FKTmA.mjs +49 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CYdinE-i.mjs} +1 -1
- package/.output/server/_ssr/failureReports-CRYHdQBQ.mjs +33 -0
- package/.output/server/_ssr/{table-QQ9cnWf2.mjs → field-BDdwYmC9.mjs} +62 -50
- package/.output/server/_ssr/getBundleChildren-DLGwrnCV.mjs +22 -0
- package/.output/server/_ssr/{input-group-Oj4kUnE4.mjs → input-group-CtXqWBma.mjs} +52 -3
- package/.output/server/_ssr/insights-BwcXtX7g.mjs +6 -0
- package/.output/server/_ssr/insights-C8ASzAF4.mjs +2311 -0
- package/.output/server/_ssr/insights-ElGkOnT7.mjs +15 -0
- package/.output/server/_ssr/insights-api-DG2TYhoW.mjs +122 -0
- package/.output/server/_ssr/insights-errors-F-7hOApm.mjs +34 -0
- package/.output/server/_ssr/insights-failures-BZaGNNij.mjs +47 -0
- package/.output/server/_ssr/insights-failures-rpc-tmzvyUYN.mjs +36 -0
- package/.output/server/_ssr/{insights-recovery-DSFM-YTL.mjs → insights-recovery-Dpk6Mfb9.mjs} +9 -2
- package/.output/server/_ssr/insights-recovery-rpc-BeiUA-UY.mjs +32 -0
- package/.output/server/_ssr/{insights-rpc-DBf0ouen.mjs → insights-rpc-CmHYtKGn.mjs} +22 -14
- package/.output/server/_ssr/insights-usage-CLUWjUFz.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-CCYPPFtN.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-CIHxaYDx.mjs +41 -0
- package/.output/server/_ssr/insightsUsage-9X2sp3ja.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-BIrvxsSd.mjs +334 -0
- package/.output/server/_ssr/insights_.distribution-BrmjQspC.mjs +5 -0
- package/.output/server/_ssr/insights_.distribution-CLPoeEKH.mjs +14 -0
- package/.output/server/_ssr/installations-BYDcd1BU.mjs +69 -0
- package/.output/server/_ssr/{installations-DQyJN-pZ.mjs → installations-CiW7qX4b.mjs} +428 -97
- package/.output/server/_ssr/installations-DfvIE7EI.mjs +5 -0
- package/.output/server/_ssr/listReleases-BitHzOYB.mjs +79 -0
- package/.output/server/_ssr/release-adoption-C2b8n-Xs.mjs +55 -0
- package/.output/server/_ssr/release-adoption-rpc-BENRIE3G.mjs +41 -0
- package/.output/server/_ssr/releaseAdoption-DW_6eea-.mjs +96 -0
- package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
- package/.output/server/_ssr/{router-C8J4nYEG.mjs → router-Cu1yxD7n.mjs} +134 -138
- package/.output/server/_ssr/routes-DZeygWIs.mjs +369 -0
- package/.output/server/_ssr/{routes-NlTRKPML.mjs → routes-lIwWp0Ch.mjs} +175 -389
- package/.output/server/_ssr/runtime.server-Dkb3vsY2.mjs +1500 -0
- package/.output/server/_ssr/schema-BKY9SqaY.mjs +25 -0
- package/.output/server/_ssr/select-dVdHxJGf.mjs +107 -0
- package/.output/server/_ssr/server-BjWNP8Qv.mjs +8330 -0
- package/.output/server/_ssr/{createSsrRpc-4rr4IJh3.mjs → sidebar-BZrY-cut.mjs} +107 -47
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/start-BXUOv_n8.mjs +4 -0
- package/.output/server/_ssr/table-BhkGaNvb.mjs +53 -0
- package/.output/server/_ssr/updateFailures-BP-A8Ppr.mjs +40 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C8bwiZr8.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-DWJb58Ez.mjs +123 -0
- package/.output/server/index.mjs +241 -164
- package/LICENSE +13 -14
- package/README.md +82 -72
- package/lib/index.d.mts +23 -11
- package/lib/vite-internal.mjs +13 -3
- package/lib/vite.d.mts +2 -0
- package/package.json +14 -14
- package/src/components/AppSidebar.tsx +100 -71
- package/src/components/ConsoleAccessPage.tsx +5 -1
- package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +99 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +8 -5
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
- package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
- package/src/components/features/insights/AppUsage.tsx +370 -0
- package/src/components/features/insights/ComparedBundlesChart.tsx +256 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +294 -37
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- package/src/components/features/insights/FailureReports.tsx +394 -0
- package/src/components/features/insights/InsightsControls.tsx +265 -102
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsInfo.tsx +48 -0
- package/src/components/features/insights/InsightsOverview.tsx +583 -167
- package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
- package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +58 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +290 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
- package/src/components/features/insights/RollbackButton.tsx +118 -0
- package/src/components/features/insights/UpdateFailures.tsx +451 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +30 -20
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/components/ui/tabs.tsx +81 -0
- package/src/index.ts +26 -19
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +189 -266
- package/src/lib/api.ts +19 -102
- package/src/lib/bundle-activity.ts +30 -0
- package/src/lib/console-access.ts +20 -0
- package/src/lib/console-features-api.ts +68 -0
- package/src/lib/console-features-rpc.ts +13 -0
- package/src/lib/console-features.ts +91 -0
- package/src/lib/insights-api.ts +56 -2
- package/src/lib/insights-errors.ts +64 -0
- package/src/lib/insights-failures-rpc.ts +36 -0
- package/src/lib/insights-failures.ts +140 -0
- package/src/lib/insights-recovery-rpc.ts +28 -8
- package/src/lib/insights-recovery.ts +10 -23
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +93 -0
- package/src/lib/insights-usage-rpc.ts +29 -0
- package/src/lib/insights-usage.ts +74 -0
- package/src/lib/insights-view.ts +38 -2
- package/src/lib/release-adoption-api.ts +214 -0
- package/src/lib/release-adoption-rpc.ts +49 -0
- package/src/lib/release-adoption.ts +159 -0
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/adminInsights.ts +124 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +36 -0
- package/src/lib/server/config.server.ts +21 -74
- package/src/lib/server/console-feature-apis.ts +59 -0
- package/src/lib/server/console-runtime.server.ts +82 -38
- package/src/lib/server/downloadBundle.ts +16 -15
- package/src/lib/server/failureReports.ts +49 -0
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseAdoption.ts +124 -0
- package/src/lib/server/releaseReachability.ts +8 -7
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +65 -0
- package/src/routeTree.gen.ts +44 -44
- package/src/router.tsx +3 -2
- package/src/routes/-installations-search.ts +32 -8
- package/src/routes/-releases-search.ts +76 -29
- package/src/routes/__root.tsx +10 -4
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +167 -104
- package/src/routes/insights.tsx +157 -60
- package/src/routes/insights_.distribution.tsx +398 -0
- package/src/routes/installations.tsx +98 -41
- package/src/styles.css +9 -0
- package/src/vite-internal.ts +15 -3
- package/src/vite.ts +2 -0
- package/.output/public/assets/InsightsErrorAlert-1cg6LDJG.js +0 -1
- package/.output/public/assets/InsightsPageHeader-oNIg_lcd.js +0 -1
- package/.output/public/assets/InsightsRolloutCard-Zsv9gBjU.js +0 -42
- package/.output/public/assets/api-keys-ChJjsbP2.js +0 -1
- package/.output/public/assets/api-keys-api-LchP65vQ.js +0 -1
- package/.output/public/assets/card-iD7iiy4T.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-C0G0fQ3J.js +0 -1
- package/.output/public/assets/createServerFn-DHc2erub.js +0 -3
- package/.output/public/assets/dialog-DX6o8qCL.js +0 -1
- package/.output/public/assets/dist-BPrb_vuw.js +0 -1
- package/.output/public/assets/index-C6Nb14xG.js +0 -10
- package/.output/public/assets/input-group-Pa3SAbEI.js +0 -1
- package/.output/public/assets/insights-C1uKy3p2.js +0 -1
- package/.output/public/assets/installations-Bj5AzZ3T.js +0 -1
- package/.output/public/assets/installations-DDTw9DSK.js +0 -2
- package/.output/public/assets/lazyRouteComponent-BN0MS05o.js +0 -1
- package/.output/public/assets/link-BG06whwB.js +0 -1
- package/.output/public/assets/loader-circle-Bbp8LGVC.js +0 -1
- package/.output/public/assets/preload-helper-B_l5jfgx.js +0 -1
- package/.output/public/assets/refresh-cw-DZgiAOMk.js +0 -1
- package/.output/public/assets/routes-9E2okfpG.js +0 -1
- package/.output/public/assets/routes-B3JviaDV.js +0 -2
- package/.output/public/assets/shield-check-DoIiaaXU.js +0 -1
- package/.output/public/assets/signing-BKy9fIdB.js +0 -1
- package/.output/public/assets/styles-C5-su40i.css +0 -2
- package/.output/public/assets/table-bpgpQkPz.js +0 -1
- package/.output/public/assets/useOpenInteractionType-4dPacbwb.js +0 -1
- package/.output/public/assets/x-Cr_sPM8E.js +0 -1
- package/.output/server/_ssr/InsightsErrorAlert-DzDVdfgW.mjs +0 -169
- package/.output/server/_ssr/InsightsPageHeader-CZ2AaQGk.mjs +0 -372
- package/.output/server/_ssr/InsightsRolloutCard-DHB2iM4t.mjs +0 -693
- package/.output/server/_ssr/api-keys-api-KSH04dLB.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-DBmgnW5o.mjs +0 -136
- package/.output/server/_ssr/api-rpc-DX5B6b-L.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DF_shCeq.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-C01EKpb-.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-Co_zHdJx.mjs +0 -12
- package/.output/server/_ssr/card-gGh8F6kQ.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-NIXXHk-e.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-C71v1GBr.mjs +0 -66
- package/.output/server/_ssr/getBundleChildren-CMGSGBWv.mjs +0 -50
- package/.output/server/_ssr/insights-oK2n5WBp.mjs +0 -406
- package/.output/server/_ssr/insights-recovery-rpc-CgeCAs2K.mjs +0 -16
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/insightsRecovery-DFkXGFjd.mjs +0 -108
- package/.output/server/_ssr/installations-DBmYuVBi.mjs +0 -29
- package/.output/server/_ssr/routes-C2RIYhyl.mjs +0 -37
- package/.output/server/_ssr/runtime.server-DLZpzphL.mjs +0 -438
- package/.output/server/_ssr/server-hgrXpJQY.mjs +0 -7468
- package/.output/server/_ssr/signing-bfOvUMSJ.mjs +0 -133
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-BAb_a_eC.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- package/src/components/features/insights/InsightsRolloutCard.tsx +0 -505
- package/src/components/features/signing/BundleSigningPage.tsx +0 -139
- package/src/lib/bundle-signing-api.ts +0 -14
- package/src/lib/bundle-signing-rpc.ts +0 -16
- package/src/lib/server/bundle-signing.server.ts +0 -23
- package/src/lib/server/deleteBundle.ts +0 -145
- package/src/lib/server/insightsRecovery.ts +0 -157
- package/src/routes/signing.tsx +0 -37
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { Link } from "@tanstack/react-router";
|
|
2
|
-
import { ChartNoAxesCombined } from "lucide-react";
|
|
2
|
+
import { ChartNoAxesCombined, ListIcon } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import { buttonVariants } from "@/components/ui/button";
|
|
5
5
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
6
|
+
import { useConsoleFeature } from "@/lib/console-features-api";
|
|
7
|
+
import type { InsightsSearch } from "@/lib/insights-search";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
6
9
|
|
|
7
10
|
export function InsightsPageHeader({
|
|
8
11
|
view,
|
|
12
|
+
overviewSearch,
|
|
9
13
|
}: {
|
|
10
|
-
readonly view: "overview" | "events";
|
|
14
|
+
readonly view: "overview" | "events" | "distribution";
|
|
15
|
+
readonly overviewSearch?: InsightsSearch;
|
|
11
16
|
}) {
|
|
17
|
+
// A self-hosted server's admin API serves events, not the overview.
|
|
18
|
+
const overview = useConsoleFeature("insightsAnalytics");
|
|
12
19
|
return (
|
|
13
20
|
<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
21
|
<SidebarTrigger className="-ml-1 size-11 lg:size-7" />
|
|
@@ -19,28 +26,36 @@ export function InsightsPageHeader({
|
|
|
19
26
|
/>
|
|
20
27
|
<h1 className="text-sm font-medium">Insights</h1>
|
|
21
28
|
</div>
|
|
22
|
-
<nav aria-label="Insights views" className="ml-auto flex gap-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
className
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
<nav aria-label="Insights views" className="ml-auto flex gap-2">
|
|
30
|
+
{overview ? (
|
|
31
|
+
<Link
|
|
32
|
+
aria-current={view === "overview" ? "page" : undefined}
|
|
33
|
+
className={cn(
|
|
34
|
+
buttonVariants({
|
|
35
|
+
className: "h-11 px-3 lg:h-8 lg:px-2.5",
|
|
36
|
+
size: "lg",
|
|
37
|
+
variant: view === "overview" ? "secondary" : "ghost",
|
|
38
|
+
}),
|
|
39
|
+
)}
|
|
40
|
+
search={overviewSearch}
|
|
41
|
+
to="/insights"
|
|
42
|
+
>
|
|
43
|
+
Overview
|
|
44
|
+
</Link>
|
|
45
|
+
) : null}
|
|
34
46
|
<Link
|
|
35
47
|
aria-current={view === "events" ? "page" : undefined}
|
|
36
|
-
className={
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
className={cn(
|
|
49
|
+
buttonVariants({
|
|
50
|
+
className: "h-11 px-3 lg:h-8 lg:px-2.5",
|
|
51
|
+
size: "lg",
|
|
52
|
+
variant: view === "events" ? "secondary" : "ghost",
|
|
53
|
+
}),
|
|
54
|
+
)}
|
|
41
55
|
to="/installations"
|
|
42
56
|
>
|
|
43
|
-
|
|
57
|
+
<ListIcon aria-hidden="true" data-icon="inline-start" />
|
|
58
|
+
All events
|
|
44
59
|
</Link>
|
|
45
60
|
</nav>
|
|
46
61
|
</header>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
2
|
+
import type { InsightsWindow } from "@/lib/insights-api";
|
|
3
|
+
import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
|
|
4
|
+
|
|
5
|
+
/** A segmented control over fixed periods, each shown by its short value. */
|
|
6
|
+
export function PeriodSelector<TPeriod extends string>({
|
|
7
|
+
label,
|
|
8
|
+
onPeriodChange,
|
|
9
|
+
period,
|
|
10
|
+
periods,
|
|
11
|
+
}: {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly onPeriodChange: (period: TPeriod) => void;
|
|
14
|
+
readonly period: TPeriod;
|
|
15
|
+
readonly periods: readonly {
|
|
16
|
+
readonly value: TPeriod;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
}[];
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<Tabs
|
|
22
|
+
value={period}
|
|
23
|
+
onValueChange={(value) => {
|
|
24
|
+
const next = periods.find((item) => item.value === value);
|
|
25
|
+
if (next) onPeriodChange(next.value);
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<TabsList aria-label={label} className="min-h-11 sm:min-h-9">
|
|
29
|
+
{periods.map(({ value, name }) => (
|
|
30
|
+
<TabsTrigger
|
|
31
|
+
key={value}
|
|
32
|
+
value={value}
|
|
33
|
+
aria-label={name}
|
|
34
|
+
className="min-w-12 px-3"
|
|
35
|
+
>
|
|
36
|
+
{value}
|
|
37
|
+
</TabsTrigger>
|
|
38
|
+
))}
|
|
39
|
+
</TabsList>
|
|
40
|
+
</Tabs>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reportingPeriods = (["24h", "7d", "30d"] as const).map((value) => ({
|
|
45
|
+
value,
|
|
46
|
+
name: usageMetrics[value].period,
|
|
47
|
+
}));
|
|
48
|
+
const usagePeriods = (Object.keys(usageMetrics) as UsageWindow[]).map(
|
|
49
|
+
(value) => ({ value, name: usageMetrics[value].period }),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export function InsightsPeriodSelector({
|
|
53
|
+
window,
|
|
54
|
+
onWindowChange,
|
|
55
|
+
}: {
|
|
56
|
+
readonly window: InsightsWindow;
|
|
57
|
+
readonly onWindowChange: (window: InsightsWindow) => void;
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<PeriodSelector
|
|
61
|
+
label="Reporting period"
|
|
62
|
+
onPeriodChange={onWindowChange}
|
|
63
|
+
period={window}
|
|
64
|
+
periods={reportingPeriods}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** App usage's periods: the reporting periods, and 12 months where daily totals cover it. */
|
|
70
|
+
export function UsagePeriodSelector({
|
|
71
|
+
window,
|
|
72
|
+
onWindowChange,
|
|
73
|
+
twelveMonths = true,
|
|
74
|
+
}: {
|
|
75
|
+
readonly window: UsageWindow;
|
|
76
|
+
readonly onWindowChange: (window: UsageWindow) => void;
|
|
77
|
+
readonly twelveMonths?: boolean;
|
|
78
|
+
}) {
|
|
79
|
+
return (
|
|
80
|
+
<PeriodSelector
|
|
81
|
+
label="Reporting period"
|
|
82
|
+
onPeriodChange={onWindowChange}
|
|
83
|
+
period={window}
|
|
84
|
+
periods={usagePeriods.filter(
|
|
85
|
+
({ value }) => twelveMonths || value !== "12m",
|
|
86
|
+
)}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -2,7 +2,13 @@ import { RefreshCw } from "lucide-react";
|
|
|
2
2
|
|
|
3
3
|
import { HashValueDisplay } from "@/components/HashValueDisplay";
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@/components/ui/card";
|
|
6
12
|
import { Separator } from "@/components/ui/separator";
|
|
7
13
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
8
14
|
import {
|
|
@@ -13,6 +19,11 @@ import {
|
|
|
13
19
|
TableHeader,
|
|
14
20
|
TableRow,
|
|
15
21
|
} from "@/components/ui/table";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_INSIGHTS_RETENTION,
|
|
24
|
+
formatDays,
|
|
25
|
+
type InsightsRetentionDays,
|
|
26
|
+
} from "@/lib/insights-retention";
|
|
16
27
|
import type {
|
|
17
28
|
InsightsEventRow,
|
|
18
29
|
InsightsInstallationViewRow,
|
|
@@ -22,24 +33,26 @@ import type {
|
|
|
22
33
|
import {
|
|
23
34
|
EventBundleTransition,
|
|
24
35
|
EventTimestamp,
|
|
25
|
-
|
|
36
|
+
EventTypeDetails,
|
|
37
|
+
HttpResponseDetails,
|
|
26
38
|
useInsightsTimeFormat,
|
|
27
39
|
} from "./EventDetails";
|
|
28
40
|
import { EventHistoryList } from "./EventHistoryList";
|
|
29
41
|
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
30
42
|
import { InsightsPagination } from "./InsightsPagination";
|
|
31
43
|
|
|
32
|
-
const getUserLabel = (event: {
|
|
33
|
-
|
|
34
|
-
readonly userId: string | null;
|
|
35
|
-
}) => event.userId ?? event.username ?? "—";
|
|
44
|
+
const getUserLabel = (event: { readonly userId: string | null }) =>
|
|
45
|
+
event.userId ?? "—";
|
|
36
46
|
|
|
47
|
+
/** A download or a failed update leaves the installation on the bundle it ran. */
|
|
37
48
|
const getLastKnownBundleId = (
|
|
38
49
|
event: InsightsEventRow | InsightsInstallationViewRow,
|
|
39
50
|
) =>
|
|
40
51
|
"lastKnownBundleId" in event
|
|
41
52
|
? event.lastKnownBundleId
|
|
42
|
-
:
|
|
53
|
+
: event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_FAILED"
|
|
54
|
+
? event.fromBundleId
|
|
55
|
+
: event.toBundleId;
|
|
43
56
|
|
|
44
57
|
export function InstallationHistoryCard({
|
|
45
58
|
error,
|
|
@@ -49,6 +62,7 @@ export function InstallationHistoryCard({
|
|
|
49
62
|
onPrevious,
|
|
50
63
|
onRefresh,
|
|
51
64
|
pageNumber,
|
|
65
|
+
retention = DEFAULT_INSIGHTS_RETENTION,
|
|
52
66
|
selectedEvent,
|
|
53
67
|
selectedInstallId,
|
|
54
68
|
}: {
|
|
@@ -59,6 +73,8 @@ export function InstallationHistoryCard({
|
|
|
59
73
|
readonly onNext: () => void;
|
|
60
74
|
readonly onPrevious: () => void;
|
|
61
75
|
readonly pageNumber: number;
|
|
76
|
+
/** How long the server keeps Insights rows. */
|
|
77
|
+
readonly retention?: InsightsRetentionDays;
|
|
62
78
|
readonly selectedEvent:
|
|
63
79
|
| InsightsEventRow
|
|
64
80
|
| InsightsInstallationViewRow
|
|
@@ -70,6 +86,15 @@ export function InstallationHistoryCard({
|
|
|
70
86
|
? getLastKnownBundleId(selectedEvent)
|
|
71
87
|
: undefined;
|
|
72
88
|
|
|
89
|
+
const pendingBundleId =
|
|
90
|
+
selectedEvent && "pendingBundleId" in selectedEvent
|
|
91
|
+
? selectedEvent.pendingBundleId
|
|
92
|
+
: selectedEvent &&
|
|
93
|
+
"type" in selectedEvent &&
|
|
94
|
+
selectedEvent.type === "UPDATE_DOWNLOADED"
|
|
95
|
+
? selectedEvent.toBundleId
|
|
96
|
+
: null;
|
|
97
|
+
|
|
73
98
|
return (
|
|
74
99
|
<Card className="@container min-h-0 min-w-0 shadow-sm">
|
|
75
100
|
<CardHeader className="p-4 sm:p-6">
|
|
@@ -82,6 +107,14 @@ export function InstallationHistoryCard({
|
|
|
82
107
|
: "Select an installation"}
|
|
83
108
|
</h2>
|
|
84
109
|
</CardTitle>
|
|
110
|
+
{selectedInstallId !== undefined ? (
|
|
111
|
+
<CardDescription>
|
|
112
|
+
Bundle changes and update failures are kept for{" "}
|
|
113
|
+
{formatDays(retention.rawDays)}; the latest report, for{" "}
|
|
114
|
+
{formatDays(retention.dailyDays)} after the installation last
|
|
115
|
+
reports.
|
|
116
|
+
</CardDescription>
|
|
117
|
+
) : null}
|
|
85
118
|
</div>
|
|
86
119
|
{onRefresh && selectedInstallId !== undefined ? (
|
|
87
120
|
<Button
|
|
@@ -138,7 +171,22 @@ export function InstallationHistoryCard({
|
|
|
138
171
|
)}
|
|
139
172
|
</dd>
|
|
140
173
|
</div>
|
|
174
|
+
{pendingBundleId ? (
|
|
175
|
+
<div className="col-span-2 min-w-0">
|
|
176
|
+
<dt className="text-xs text-muted-foreground">
|
|
177
|
+
Downloaded · Pending apply
|
|
178
|
+
</dt>
|
|
179
|
+
<dd className="mt-1 text-xs">
|
|
180
|
+
<HashValueDisplay value={pendingBundleId} />
|
|
181
|
+
</dd>
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
141
184
|
</dl>
|
|
185
|
+
{selectedEvent.httpResponse ? (
|
|
186
|
+
<div className="mt-4">
|
|
187
|
+
<HttpResponseDetails event={selectedEvent} />
|
|
188
|
+
</div>
|
|
189
|
+
) : null}
|
|
142
190
|
</section>
|
|
143
191
|
<Separator />
|
|
144
192
|
</>
|
|
@@ -192,7 +240,7 @@ export function InstallationHistoryCard({
|
|
|
192
240
|
/>
|
|
193
241
|
</TableCell>
|
|
194
242
|
<TableCell>
|
|
195
|
-
<
|
|
243
|
+
<EventTypeDetails event={event} />
|
|
196
244
|
</TableCell>
|
|
197
245
|
<TableCell className="whitespace-normal text-xs">
|
|
198
246
|
<div className="flex flex-col gap-2">
|
|
@@ -226,8 +274,8 @@ export function InstallationHistoryCard({
|
|
|
226
274
|
) : (
|
|
227
275
|
<>
|
|
228
276
|
<div className="p-6 text-sm text-muted-foreground">
|
|
229
|
-
{pageNumber > 1
|
|
230
|
-
? "No bundle changes
|
|
277
|
+
{pageNumber > 1
|
|
278
|
+
? "No older bundle changes."
|
|
231
279
|
: "No bundle changes recorded yet."}
|
|
232
280
|
</div>
|
|
233
281
|
{history && (pageNumber > 1 || history.nextCursor) ? (
|
|
@@ -17,7 +17,7 @@ const getLastKnownBundleId = (event: InsightsInstallationViewRow) =>
|
|
|
17
17
|
event.lastKnownBundleId;
|
|
18
18
|
|
|
19
19
|
const getUserLabel = (event: InsightsInstallationViewRow) =>
|
|
20
|
-
event.userId ??
|
|
20
|
+
event.userId ?? "—";
|
|
21
21
|
|
|
22
22
|
export function InstallationMatchesCard({
|
|
23
23
|
error,
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { ChartLine } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardFooter,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@/components/ui/card";
|
|
12
|
+
import { Separator } from "@/components/ui/separator";
|
|
13
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
14
|
+
import type {
|
|
15
|
+
BundleActivityInput,
|
|
16
|
+
BundleActivityReport,
|
|
17
|
+
} from "@/lib/bundle-activity";
|
|
18
|
+
import { useBundleActivityQuery } from "@/lib/bundle-activity";
|
|
19
|
+
import { useConsoleFeature } from "@/lib/console-features-api";
|
|
20
|
+
import { useUpdateFailuresQuery } from "@/lib/insights-api";
|
|
21
|
+
import { failureRate, formatRate } from "@/lib/insights-failures";
|
|
22
|
+
import { adoptionWindow } from "@/lib/insights-search";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
import type { ReleaseColumn } from "../FeatureSlots";
|
|
26
|
+
import { InsightsInfo } from "./InsightsInfo";
|
|
27
|
+
|
|
28
|
+
const activityStats = [
|
|
29
|
+
{ label: "Downloads", field: "downloads", color: "text-primary/85" },
|
|
30
|
+
{ label: "Active days", field: "activeDays", color: "text-success/85" },
|
|
31
|
+
{ label: "Known crashes", field: "failedLaunches", color: "text-warning/85" },
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
const crashRate = (report: BundleActivityReport): string => {
|
|
35
|
+
const attempts = report.activeDays + report.failedLaunches;
|
|
36
|
+
return attempts === 0
|
|
37
|
+
? "—"
|
|
38
|
+
: `${((report.failedLaunches / attempts) * 100).toFixed(2)}%`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function ReleaseMetricsInfo() {
|
|
42
|
+
return (
|
|
43
|
+
<InsightsInfo label="About release insight metrics">
|
|
44
|
+
Active days count each installation once for each UTC day it launched this
|
|
45
|
+
release. Known crashes are reported OTA launch failures that triggered
|
|
46
|
+
recovery. The rate is known crashes divided by active days plus known
|
|
47
|
+
crashes.
|
|
48
|
+
</InsightsInfo>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function BundleMovementSummary({
|
|
53
|
+
report,
|
|
54
|
+
loading = false,
|
|
55
|
+
input,
|
|
56
|
+
variant = "inline",
|
|
57
|
+
}: {
|
|
58
|
+
readonly report?: BundleActivityReport;
|
|
59
|
+
readonly loading?: boolean;
|
|
60
|
+
readonly input?: BundleActivityInput;
|
|
61
|
+
readonly variant?: "inline" | "card";
|
|
62
|
+
}) {
|
|
63
|
+
if (!report) {
|
|
64
|
+
return loading ? (
|
|
65
|
+
<Skeleton
|
|
66
|
+
aria-label="Loading release insights"
|
|
67
|
+
className={variant === "card" ? "h-16 w-full" : "h-4 w-40"}
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<span
|
|
71
|
+
aria-label="Release insights unavailable"
|
|
72
|
+
className="text-sm text-muted-foreground"
|
|
73
|
+
>
|
|
74
|
+
—
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const metrics = (
|
|
79
|
+
<dl
|
|
80
|
+
className={cn(
|
|
81
|
+
variant === "card"
|
|
82
|
+
? "grid grid-cols-[1fr_1fr_auto] gap-4"
|
|
83
|
+
: "flex min-w-[220px] flex-wrap items-baseline gap-x-3 gap-y-1",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{activityStats.map(({ label, field, color }) => (
|
|
87
|
+
<div
|
|
88
|
+
key={field}
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex",
|
|
91
|
+
variant === "card"
|
|
92
|
+
? "min-w-0 flex-col gap-1"
|
|
93
|
+
: "items-baseline gap-1.5 whitespace-nowrap",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<dt className="text-xs text-muted-foreground">{label}</dt>
|
|
97
|
+
<dd
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex flex-wrap items-baseline gap-x-1.5 tabular-nums",
|
|
100
|
+
variant === "card"
|
|
101
|
+
? "text-xl font-semibold"
|
|
102
|
+
: "text-sm font-medium",
|
|
103
|
+
report[field] > 0 ? color : "text-muted-foreground",
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{report[field].toLocaleString()}
|
|
107
|
+
{field === "failedLaunches" ? (
|
|
108
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
109
|
+
({crashRate(report)})
|
|
110
|
+
</span>
|
|
111
|
+
) : null}
|
|
112
|
+
</dd>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</dl>
|
|
116
|
+
);
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
"flex",
|
|
121
|
+
variant === "card" ? "flex-col gap-2" : "items-center gap-1",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{input ? (
|
|
125
|
+
<Link
|
|
126
|
+
className="min-w-0 rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
127
|
+
to="/insights"
|
|
128
|
+
search={{
|
|
129
|
+
healthPlatform: input.platform,
|
|
130
|
+
healthChannel: input.channel,
|
|
131
|
+
releaseId: input.releaseId,
|
|
132
|
+
bundleWindow: "7d",
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{metrics}
|
|
136
|
+
</Link>
|
|
137
|
+
) : (
|
|
138
|
+
metrics
|
|
139
|
+
)}
|
|
140
|
+
{variant === "inline" ? <ReleaseMetricsInfo /> : null}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The release's failed downloads and installs since its first report, and their rate. */
|
|
146
|
+
export function BundleDownloadFailures({
|
|
147
|
+
input,
|
|
148
|
+
}: {
|
|
149
|
+
readonly input: BundleActivityInput;
|
|
150
|
+
}) {
|
|
151
|
+
const query = useUpdateFailuresQuery({
|
|
152
|
+
platform: input.platform,
|
|
153
|
+
channel: input.channel,
|
|
154
|
+
releaseId: input.releaseId,
|
|
155
|
+
});
|
|
156
|
+
const report = query.data;
|
|
157
|
+
return (
|
|
158
|
+
<div className="flex items-center gap-1">
|
|
159
|
+
{report ? (
|
|
160
|
+
<dl>
|
|
161
|
+
<div className="flex flex-wrap items-baseline gap-x-1.5">
|
|
162
|
+
<dt className="text-xs text-muted-foreground">Download failures</dt>
|
|
163
|
+
<dd
|
|
164
|
+
className={cn(
|
|
165
|
+
"flex items-baseline gap-1.5 text-sm font-medium tabular-nums",
|
|
166
|
+
report.failedUpdates > 0
|
|
167
|
+
? "text-warning/85"
|
|
168
|
+
: "text-muted-foreground",
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{report.failedUpdates.toLocaleString()}
|
|
172
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
173
|
+
({formatRate(failureRate(report))})
|
|
174
|
+
</span>
|
|
175
|
+
</dd>
|
|
176
|
+
</div>
|
|
177
|
+
</dl>
|
|
178
|
+
) : query.isPending ? (
|
|
179
|
+
<Skeleton aria-label="Loading download failures" className="h-4 w-40" />
|
|
180
|
+
) : (
|
|
181
|
+
<span
|
|
182
|
+
aria-label="Download failures unavailable"
|
|
183
|
+
className="text-sm text-muted-foreground"
|
|
184
|
+
>
|
|
185
|
+
—
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
<InsightsInfo label="About download failures">
|
|
189
|
+
Reported failures to download or install this release, since its first
|
|
190
|
+
report; each client reports one at most once a UTC day. The rate is the
|
|
191
|
+
failure rate of update attempts: failures divided by failures plus
|
|
192
|
+
download reports.
|
|
193
|
+
</InsightsInfo>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The bundle detail's Insights card, a section of the release editor: activity and download failures. */
|
|
199
|
+
export function BundleInsightsSummary({
|
|
200
|
+
input,
|
|
201
|
+
}: {
|
|
202
|
+
readonly input: BundleActivityInput;
|
|
203
|
+
}) {
|
|
204
|
+
const query = useBundleActivityQuery([input]);
|
|
205
|
+
return (
|
|
206
|
+
<Card>
|
|
207
|
+
<CardHeader className="flex-row items-center justify-between px-4 pt-4 pb-3">
|
|
208
|
+
<CardTitle className="text-sm font-medium">Insights</CardTitle>
|
|
209
|
+
<ReleaseMetricsInfo />
|
|
210
|
+
</CardHeader>
|
|
211
|
+
<CardContent className="flex flex-col gap-3 px-4 pb-4">
|
|
212
|
+
<BundleMovementSummary
|
|
213
|
+
variant="card"
|
|
214
|
+
input={input}
|
|
215
|
+
loading={query.isFetching}
|
|
216
|
+
report={query.data?.[input.releaseId]}
|
|
217
|
+
/>
|
|
218
|
+
<Separator />
|
|
219
|
+
<BundleDownloadFailures input={input} />
|
|
220
|
+
</CardContent>
|
|
221
|
+
<CardFooter className="px-4 pb-4">
|
|
222
|
+
<Link
|
|
223
|
+
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
224
|
+
to="/insights"
|
|
225
|
+
search={{
|
|
226
|
+
healthPlatform: input.platform,
|
|
227
|
+
healthChannel: input.channel,
|
|
228
|
+
releaseId: input.releaseId,
|
|
229
|
+
bundleWindow: adoptionWindow(input.releaseId),
|
|
230
|
+
healthChart: "adoption",
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<ChartLine aria-hidden="true" data-icon="inline-start" />
|
|
234
|
+
View adoption
|
|
235
|
+
</Link>
|
|
236
|
+
</CardFooter>
|
|
237
|
+
</Card>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The release editor's Insights card where the console reads no release
|
|
243
|
+
* activity, as through a self-hosted server's admin API: download failures
|
|
244
|
+
* alone. With release activity, its card shows them.
|
|
245
|
+
*/
|
|
246
|
+
export function ReleaseFailuresSection({
|
|
247
|
+
input,
|
|
248
|
+
}: {
|
|
249
|
+
readonly input: BundleActivityInput;
|
|
250
|
+
}) {
|
|
251
|
+
const activity = useConsoleFeature("insightsAnalytics");
|
|
252
|
+
if (activity) return null;
|
|
253
|
+
return (
|
|
254
|
+
<Card>
|
|
255
|
+
<CardHeader className="px-4 pt-4 pb-3">
|
|
256
|
+
<CardTitle className="text-sm font-medium">Insights</CardTitle>
|
|
257
|
+
</CardHeader>
|
|
258
|
+
<CardContent className="px-4 pb-4">
|
|
259
|
+
<BundleDownloadFailures input={input} />
|
|
260
|
+
</CardContent>
|
|
261
|
+
</Card>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* One release's activity in the Bundles page's Insights column. Every cell
|
|
267
|
+
* asks for the page's releases under one query key, so the page makes one
|
|
268
|
+
* request.
|
|
269
|
+
*/
|
|
270
|
+
function ReleaseActivityCell({
|
|
271
|
+
releaseId,
|
|
272
|
+
releases,
|
|
273
|
+
}: {
|
|
274
|
+
readonly releaseId: string;
|
|
275
|
+
readonly releases: readonly BundleActivityInput[];
|
|
276
|
+
}) {
|
|
277
|
+
const query = useBundleActivityQuery(releases);
|
|
278
|
+
return (
|
|
279
|
+
<BundleMovementSummary
|
|
280
|
+
input={releases.find((release) => release.releaseId === releaseId)}
|
|
281
|
+
loading={query.isFetching}
|
|
282
|
+
report={query.data?.[releaseId]}
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The Bundles page's Insights column: each release's downloads, active days, and crashes. */
|
|
288
|
+
export const releaseActivityColumn: ReleaseColumn = {
|
|
289
|
+
Cell: ReleaseActivityCell,
|
|
290
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_INSIGHTS_RETENTION,
|
|
4
|
+
formatDays,
|
|
5
|
+
type InsightsRetentionDays,
|
|
6
|
+
} from "@/lib/insights-retention";
|
|
7
|
+
import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
|
|
8
|
+
|
|
9
|
+
import { EstimatedCount } from "./EstimatedCount";
|
|
10
|
+
import { InsightsInfo } from "./InsightsInfo";
|
|
11
|
+
|
|
12
|
+
export function ReportingDevicesSummary({
|
|
13
|
+
window,
|
|
14
|
+
count,
|
|
15
|
+
isPending,
|
|
16
|
+
partial,
|
|
17
|
+
retention = DEFAULT_INSIGHTS_RETENTION,
|
|
18
|
+
}: {
|
|
19
|
+
readonly window: UsageWindow;
|
|
20
|
+
readonly count: number | undefined;
|
|
21
|
+
readonly isPending: boolean;
|
|
22
|
+
readonly partial: boolean;
|
|
23
|
+
readonly retention?: InsightsRetentionDays;
|
|
24
|
+
}) {
|
|
25
|
+
const metric = usageMetrics[window];
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
aria-label={`${metric.label} over ${metric.period}`}
|
|
29
|
+
role="region"
|
|
30
|
+
className="flex items-center gap-3"
|
|
31
|
+
>
|
|
32
|
+
{isPending ? (
|
|
33
|
+
<Skeleton aria-label="Loading active users" className="h-9 w-12" />
|
|
34
|
+
) : (
|
|
35
|
+
<p className="text-4xl font-semibold tracking-tight tabular-nums">
|
|
36
|
+
{count === undefined ? (
|
|
37
|
+
"—"
|
|
38
|
+
) : partial ? (
|
|
39
|
+
`≥${count.toLocaleString()}`
|
|
40
|
+
) : (
|
|
41
|
+
<EstimatedCount value={count} />
|
|
42
|
+
)}
|
|
43
|
+
</p>
|
|
44
|
+
)}
|
|
45
|
+
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
46
|
+
<span>{metric.label}</span>
|
|
47
|
+
<InsightsInfo label={`How ${metric.label} is counted`}>
|
|
48
|
+
Unique app installations that reported activity in the last{" "}
|
|
49
|
+
{metric.period}, including no-change reports. Filtered by platform,
|
|
50
|
+
channel, and app version. The chart counts unique reporting
|
|
51
|
+
installations per {metric.interval}. Unique counts are estimates,
|
|
52
|
+
typically within about 3%. On DynamoDB and Firestore the server
|
|
53
|
+
updates totals in batches: when the Console cannot write to the
|
|
54
|
+
database, totals can leave out up to a minute of the latest reports.
|
|
55
|
+
Hourly counts are kept for {formatDays(retention.rawDays)} and daily
|
|
56
|
+
counts for {formatDays(retention.dailyDays)}, so 12 months counts
|
|
57
|
+
whole UTC days.
|
|
58
|
+
{partial
|
|
59
|
+
? " Only part of the history is available; the total is a lower bound."
|
|
60
|
+
: ""}
|
|
61
|
+
</InsightsInfo>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|