@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.21
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-D0LPL1Gy.js +1 -0
- package/.output/public/assets/HashValueDisplay-BmuB4FW-.js +1 -0
- package/.output/public/assets/InsightsInfo--v4SC07c.js +1 -0
- package/.output/public/assets/InsightsPageHeader-dU83CAQY.js +1 -0
- package/.output/public/assets/NotFoundPage-B5mhGG2a.js +1 -0
- package/.output/public/assets/api-D-vOVlQ4.js +1 -0
- package/.output/public/assets/api-keys-Csh8ARKU.js +1 -0
- package/.output/public/assets/api-keys-OMPUoA_A.js +1 -0
- package/.output/public/assets/arrow-left-Bwq9X1eS.js +1 -0
- package/.output/public/assets/arrow-up-right-BkOVn4if.js +1 -0
- package/.output/public/assets/card-C_7d6O9a.js +1 -0
- package/.output/public/assets/console-features-api-C1jRZTGw.js +1 -0
- package/.output/public/assets/dialog-DE8wFN_P.js +1 -0
- package/.output/public/assets/dist-C7ENGzpS.js +1 -0
- package/.output/public/assets/empty-CVd5NlQD.js +1 -0
- package/.output/public/assets/field-C06q1iC9.js +1 -0
- package/.output/public/assets/index-BeWlQtO6.js +11 -0
- package/.output/public/assets/input-group-Bi61biIA.js +1 -0
- package/.output/public/assets/insights-BjnAtC0f.js +2 -0
- package/.output/public/assets/insights-D86m11yH.js +42 -0
- package/.output/public/assets/insights-DpYstMoY.js +1 -0
- package/.output/public/assets/insights-recovery-rpc-BIm5_ALc.js +1 -0
- package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
- package/.output/public/assets/insights-usage-rpc-VHj8vyBt.js +1 -0
- package/.output/public/assets/insights_.distribution-ADcrX48c.js +2 -0
- package/.output/public/assets/insights_.distribution-CuuJUN6Y.js +1 -0
- package/.output/public/assets/insights_.distribution-DJRjuXsO.js +1 -0
- package/.output/public/assets/installations-BSBBzqWs.js +2 -0
- package/.output/public/assets/installations-DqIE6WcO.js +1 -0
- package/.output/public/assets/installations-UsELbDvw.js +1 -0
- package/.output/public/assets/loader-circle-CnCBxzHp.js +1 -0
- package/.output/public/assets/mutation-Csp7xUNh.js +1 -0
- package/.output/public/assets/preload-helper-B-cOmpLH.js +1 -0
- package/.output/public/assets/refresh-cw-FF1NtzlQ.js +1 -0
- package/.output/public/assets/routes-CoN9XHP_.js +2 -0
- package/.output/public/assets/routes-DT1jB0Zi.js +1 -0
- package/.output/public/assets/sidebar-D8mJRF15.js +3 -0
- package/.output/public/assets/styles-Bwt7v6U5.css +2 -0
- package/.output/public/assets/table-WBgfvgXg.js +1 -0
- package/.output/public/assets/useCompositeListItem-CloBmDwn.js +1 -0
- package/.output/public/assets/useMutation-DO8cvsTx.js +1 -0
- package/.output/public/assets/useOpenInteractionType-ReGr9GzJ.js +1 -0
- package/.output/public/assets/valueToPercent-BvPAi4bP.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-DMRD4e4f.mjs} +51 -47
- package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +3803 -4023
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +141 -98
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1159 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +46 -32
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14350 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__history.mjs +68 -41
- package/.output/server/_libs/tanstack__query-core.mjs +1618 -309
- package/.output/server/_libs/tanstack__react-query.mjs +262 -22
- package/.output/server/_libs/verkit.mjs +99 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/ConsoleFeatureUnavailable-Du7jNPNS.mjs +58 -0
- package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-GQvFkXoi.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-Do7QSaFR.mjs +37 -0
- package/.output/server/_ssr/InsightsPageHeader-DSUWUWRc.mjs +154 -0
- package/.output/server/_ssr/NotFoundPage-DsF4Vrhg.mjs +37 -0
- package/.output/server/_ssr/api-BEiVs0H7.mjs +203 -0
- package/.output/server/_ssr/api-keys-BrvBWGRf.mjs +5 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-K5v32nf7.mjs} +49 -9
- package/.output/server/_ssr/api-keys-rpc-B96bBllm.mjs +52 -0
- package/.output/server/_ssr/api-rpc-DcUe280B.mjs +296 -0
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-C68W56DP.mjs} +6 -6
- package/.output/server/_ssr/auth.server-DM1J6NwR.mjs +5712 -0
- package/.output/server/_ssr/bundleActivity-CpuY9CTP.mjs +19 -0
- package/.output/server/_ssr/card-CPAEKx0w.mjs +49 -0
- package/.output/server/_ssr/config-nsIcsHh8.mjs +15 -0
- package/.output/server/_ssr/config.server-DDCZk3Sq.mjs +36 -0
- package/.output/server/_ssr/console-access-Da-2Qd7q.mjs +17 -0
- package/.output/server/_ssr/console-features-Dzu2iUGD.mjs +55 -0
- package/.output/server/_ssr/console-features-api-xAsSgFa_.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-qARGpB24.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-DS-nw-pA.mjs → dialog-q__BJ7mQ.mjs} +2 -2
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-Cxo9ghYv.mjs} +140 -6
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-BW9I-pMQ.mjs} +10 -10
- package/.output/server/_ssr/empty-DcBTAooP.mjs +49 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CYdinE-i.mjs} +1 -1
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-QCDBVpXA.mjs} +63 -51
- package/.output/server/_ssr/getBundleChildren-DIAGCF4a.mjs +22 -0
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-hCk2nUoU.mjs} +2 -2
- package/.output/server/_ssr/insights-BfBO0RFr.mjs +1238 -0
- package/.output/server/_ssr/insights-DsAU3-TB.mjs +14 -0
- package/.output/server/_ssr/insights-RIfg447W.mjs +5 -0
- package/.output/server/_ssr/insights-api-CDn-QQHU.mjs +105 -0
- package/.output/server/_ssr/insights-failures-BBGMhuUr.mjs +44 -0
- package/.output/server/_ssr/insights-failures-rpc-rA2dctK2.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-Dpk6Mfb9.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-BCaF-xSd.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-rpc-CcgYyUHk.mjs +20 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-keycmwzw.mjs} +22 -14
- package/.output/server/_ssr/insights-search-BRg5VsNe.mjs +25 -0
- package/.output/server/_ssr/insights-usage-cbw9n2Fq.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-BKgV1QDf.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-D-JYachy.mjs +41 -0
- package/.output/server/_ssr/insightsRecovery-B7oMc8d8.mjs +45 -0
- package/.output/server/_ssr/insightsUsage-Oa7kk5ek.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-BIqOqRW0.mjs +335 -0
- package/.output/server/_ssr/insights_.distribution-CuUrDa7P.mjs +5 -0
- package/.output/server/_ssr/insights_.distribution-RT_6x_ud.mjs +14 -0
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-BO5CjX2z.mjs} +517 -54
- package/.output/server/_ssr/installations-BjaHiIfh.mjs +5 -0
- package/.output/server/_ssr/installations-Dt5luA-N.mjs +69 -0
- package/.output/server/_ssr/listReleases-BitHzOYB.mjs +79 -0
- package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-DfVAixxe.mjs} +135 -139
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-C6LstNp8.mjs} +176 -455
- package/.output/server/_ssr/routes-Q3Uhmwwz.mjs +292 -0
- package/.output/server/_ssr/runtime.server-fL0o6I2b.mjs +1459 -0
- package/.output/server/_ssr/schema-BKY9SqaY.mjs +25 -0
- package/.output/server/_ssr/select-CUlInmjk.mjs +107 -0
- package/.output/server/_ssr/server-C43wq9ft.mjs +8330 -0
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-DKbx2ach.mjs} +117 -63
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/start-BXUOv_n8.mjs +4 -0
- package/.output/server/_ssr/table-NGWV8B-S.mjs +53 -0
- package/.output/server/_ssr/updateFailures-Jwe2Baab.mjs +24 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C8bwiZr8.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-WQ5qGn8n.mjs +124 -0
- package/.output/server/index.mjs +267 -162
- 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/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +117 -37
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- 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 +176 -173
- 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 +52 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +188 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
- package/src/components/features/insights/UpdateFailures.tsx +319 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +30 -7
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/empty.tsx +101 -0
- 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 +31 -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 +37 -1
- package/src/lib/insights-failures-rpc.ts +21 -0
- package/src/lib/insights-failures.ts +129 -0
- package/src/lib/insights-recovery-rpc.ts +51 -0
- package/src/lib/insights-recovery.ts +68 -0
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +58 -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/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 +37 -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/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsRecovery.ts +55 -0
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseReachability.ts +8 -7
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +31 -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 +100 -59
- package/src/routes/insights_.distribution.tsx +398 -0
- package/src/routes/installations.tsx +98 -41
- package/src/vite-internal.ts +15 -3
- package/src/vite.ts +2 -0
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
- package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
- package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
- package/.output/public/assets/card-Dhr_eFYj.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
- package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
- package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
- package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
- package/.output/public/assets/dist-C_NydL0e.js +0 -1
- package/.output/public/assets/index-DdAz-CXv.js +0 -10
- package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
- package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
- package/.output/public/assets/installations-CE33wgbY.js +0 -2
- package/.output/public/assets/installations-CStZja8x.js +0 -1
- package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
- package/.output/public/assets/link-CI8idAUi.js +0 -1
- package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
- package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
- package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
- package/.output/public/assets/routes-DGVXdzpw.js +0 -2
- package/.output/public/assets/routes-DTlwOr8I.js +0 -1
- package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
- package/.output/public/assets/signing-CMf4aqDO.js +0 -1
- package/.output/public/assets/styles-BCvbtKsm.css +0 -2
- package/.output/public/assets/table-2vcV89QB.js +0 -1
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
- package/.output/public/assets/x-DCd0j8c6.js +0 -1
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
- package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
- package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-BTGwBgzx.mjs +0 -66
- package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
- package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
- package/.output/server/_ssr/server-D_C2rMzf.mjs +0 -7468
- package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- 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/routes/signing.tsx +0 -37
|
@@ -48,7 +48,11 @@ export function ConsoleAccessPage({
|
|
|
48
48
|
setSigningOut(true);
|
|
49
49
|
setError(null);
|
|
50
50
|
try {
|
|
51
|
-
const response = await fetch("/api/auth/sign-out", {
|
|
51
|
+
const response = await fetch("/api/auth/sign-out", {
|
|
52
|
+
body: "{}",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
method: "POST",
|
|
55
|
+
});
|
|
52
56
|
if (!response.ok) {
|
|
53
57
|
throw new Error("Sign-out failed.");
|
|
54
58
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ArrowUpRight } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { NotFoundPage } from "@/components/NotFoundPage";
|
|
4
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Empty,
|
|
7
|
+
EmptyContent,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyHeader,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from "@/components/ui/empty";
|
|
12
|
+
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
13
|
+
import { consoleFeatures, unavailableReason } from "@/lib/console-features";
|
|
14
|
+
import {
|
|
15
|
+
notFoundFeature,
|
|
16
|
+
useConsoleFeatures,
|
|
17
|
+
} from "@/lib/console-features-api";
|
|
18
|
+
|
|
19
|
+
export const CONSOLE_DEPLOYMENT_GUIDE_URL =
|
|
20
|
+
"https://hot-updater.dev/docs/guides/console-deployment";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A feature route's not-found state: the plugin to add, or, for a feature a
|
|
24
|
+
* self-hosted server does not serve, the database config it needs.
|
|
25
|
+
*/
|
|
26
|
+
export function ConsoleFeatureUnavailable({
|
|
27
|
+
data,
|
|
28
|
+
}: {
|
|
29
|
+
readonly data?: unknown;
|
|
30
|
+
}) {
|
|
31
|
+
const feature = notFoundFeature(data);
|
|
32
|
+
const remote = useConsoleFeatures().data?.remote === true;
|
|
33
|
+
if (feature === undefined) return <NotFoundPage />;
|
|
34
|
+
|
|
35
|
+
const { label, plugin } = consoleFeatures[feature];
|
|
36
|
+
const pluginCall = <code className="font-mono">{`${plugin}()`}</code>;
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex h-svh min-h-0 flex-col">
|
|
39
|
+
<header className="sticky top-0 z-10 flex shrink-0 items-center gap-3 border-b bg-background px-3 py-3 sm:min-h-12 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
|
|
40
|
+
<SidebarTrigger className="-ml-1" />
|
|
41
|
+
<h1 className="text-sm font-medium">{label}</h1>
|
|
42
|
+
</header>
|
|
43
|
+
<div className="flex min-h-0 min-w-0 flex-1 overflow-y-auto bg-muted/5 p-3 sm:p-6">
|
|
44
|
+
<Empty>
|
|
45
|
+
<EmptyHeader>
|
|
46
|
+
{unavailableReason(feature, { remote }) === "remote" ? (
|
|
47
|
+
<>
|
|
48
|
+
<EmptyTitle>Needs the server's database</EmptyTitle>
|
|
49
|
+
<EmptyDescription>
|
|
50
|
+
This console reaches a self-hosted server through its admin
|
|
51
|
+
API, which does not serve this page. Configure the console
|
|
52
|
+
with the database the server uses.
|
|
53
|
+
</EmptyDescription>
|
|
54
|
+
</>
|
|
55
|
+
) : (
|
|
56
|
+
<>
|
|
57
|
+
<EmptyTitle>{label} not installed</EmptyTitle>
|
|
58
|
+
<EmptyDescription>
|
|
59
|
+
Add {pluginCall} to <code className="font-mono">plugins</code>{" "}
|
|
60
|
+
where you create the server and in the console config.
|
|
61
|
+
</EmptyDescription>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</EmptyHeader>
|
|
65
|
+
<EmptyContent>
|
|
66
|
+
<a
|
|
67
|
+
className={buttonVariants({ variant: "outline" })}
|
|
68
|
+
href={CONSOLE_DEPLOYMENT_GUIDE_URL}
|
|
69
|
+
rel="noreferrer"
|
|
70
|
+
target="_blank"
|
|
71
|
+
>
|
|
72
|
+
Console setup guide
|
|
73
|
+
<ArrowUpRight aria-hidden="true" data-icon="inline-end" />
|
|
74
|
+
</a>
|
|
75
|
+
</EmptyContent>
|
|
76
|
+
</Empty>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -3,14 +3,14 @@ import { Link } from "@tanstack/react-router";
|
|
|
3
3
|
import { buttonVariants } from "@/components/ui/button";
|
|
4
4
|
|
|
5
5
|
const homeSearch = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
targetAppVersion: undefined,
|
|
9
|
-
page: undefined,
|
|
10
|
-
after: undefined,
|
|
11
|
-
before: undefined,
|
|
6
|
+
afterReleaseId: undefined,
|
|
7
|
+
beforeReleaseId: undefined,
|
|
12
8
|
bundleId: undefined,
|
|
13
|
-
|
|
9
|
+
channelId: undefined,
|
|
10
|
+
enabled: undefined,
|
|
11
|
+
platform: undefined,
|
|
12
|
+
releaseId: undefined,
|
|
13
|
+
scopeKey: undefined,
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
16
16
|
export function NotFoundPage() {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ConsoleFeature,
|
|
5
|
+
type ConsoleFeatures,
|
|
6
|
+
consoleFeatures,
|
|
7
|
+
} from "@/lib/console-features";
|
|
8
|
+
import { useConsoleFeatures } from "@/lib/console-features-api";
|
|
9
|
+
|
|
10
|
+
import { apiKeysFeatureUi } from "./api-keys/apiKeysFeatureUi";
|
|
11
|
+
import { insightsFeatureUi } from "./insights/insightsFeatureUi";
|
|
12
|
+
|
|
13
|
+
/** A release, as the Bundles page and the release editor hand it to slots. */
|
|
14
|
+
export interface ReleaseSlotInput {
|
|
15
|
+
readonly releaseId: string;
|
|
16
|
+
readonly platform: "ios" | "android";
|
|
17
|
+
readonly channel: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A column a feature adds to the Bundles page's release list. `Cell` renders
|
|
22
|
+
* one release, and gets the page's releases too, so a feature can read them
|
|
23
|
+
* all in one request that every cell shares.
|
|
24
|
+
*/
|
|
25
|
+
export interface ReleaseColumn {
|
|
26
|
+
readonly Cell: ComponentType<{
|
|
27
|
+
readonly releaseId: string;
|
|
28
|
+
readonly releases: readonly ReleaseSlotInput[];
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A navigation item a feature adds below Bundles. */
|
|
33
|
+
export interface FeatureNavigation {
|
|
34
|
+
readonly icon: ReactNode;
|
|
35
|
+
/** The pathnames where the item shows as active. */
|
|
36
|
+
readonly paths: readonly string[];
|
|
37
|
+
readonly link: (props: {
|
|
38
|
+
readonly features: ConsoleFeatures;
|
|
39
|
+
readonly onClick: () => void;
|
|
40
|
+
}) => ReactElement;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What a feature adds to the console's pages while it is on. */
|
|
44
|
+
export interface ConsoleFeatureUi {
|
|
45
|
+
readonly navigation?: FeatureNavigation;
|
|
46
|
+
readonly releaseColumn?: ReleaseColumn;
|
|
47
|
+
/** A section of the release editor, above its settings. */
|
|
48
|
+
readonly releaseSection?: ComponentType<{
|
|
49
|
+
readonly release: ReleaseSlotInput;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What each feature adds, keyed like `consoleFeatures`. */
|
|
54
|
+
export type ConsoleFeatureUiRegistry = {
|
|
55
|
+
readonly [F in ConsoleFeature]?: ConsoleFeatureUi;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const consoleFeatureUi: ConsoleFeatureUiRegistry = {
|
|
59
|
+
...insightsFeatureUi,
|
|
60
|
+
...apiKeysFeatureUi,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const featureIds = Object.keys(consoleFeatures) as ConsoleFeature[];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What the features that are on add, in the registry's order, with each
|
|
67
|
+
* feature's label; nothing until the features load.
|
|
68
|
+
*/
|
|
69
|
+
export const useFeatureUi = (): readonly (ConsoleFeatureUi & {
|
|
70
|
+
readonly feature: ConsoleFeature;
|
|
71
|
+
readonly label: string;
|
|
72
|
+
readonly features: ConsoleFeatures;
|
|
73
|
+
})[] => {
|
|
74
|
+
const features = useConsoleFeatures().data?.features;
|
|
75
|
+
if (features === undefined) return [];
|
|
76
|
+
return featureIds.flatMap((feature) => {
|
|
77
|
+
const ui = consoleFeatureUi[feature];
|
|
78
|
+
return ui !== undefined && features[feature]
|
|
79
|
+
? [{ ...ui, feature, label: consoleFeatures[feature].label, features }]
|
|
80
|
+
: [];
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The release list's feature columns, with their headers. */
|
|
85
|
+
export const useReleaseColumns = () =>
|
|
86
|
+
useFeatureUi().flatMap(({ feature, label, releaseColumn }) =>
|
|
87
|
+
releaseColumn === undefined ? [] : [{ ...releaseColumn, feature, label }],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/** The release editor's feature sections, for one release. */
|
|
91
|
+
export function ReleaseSections({
|
|
92
|
+
release,
|
|
93
|
+
}: {
|
|
94
|
+
readonly release: ReleaseSlotInput;
|
|
95
|
+
}) {
|
|
96
|
+
return useFeatureUi().map(({ feature, releaseSection: Section }) =>
|
|
97
|
+
Section === undefined ? null : <Section key={feature} release={release} />,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { KeyRound } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import type { ConsoleFeatureUiRegistry } from "../FeatureSlots";
|
|
5
|
+
|
|
6
|
+
/** What API key management adds: its navigation item. */
|
|
7
|
+
export const apiKeysFeatureUi = {
|
|
8
|
+
apiKeys: {
|
|
9
|
+
navigation: {
|
|
10
|
+
icon: <KeyRound />,
|
|
11
|
+
paths: ["/api-keys"],
|
|
12
|
+
link: ({ onClick }) => <Link to="/api-keys" onClick={onClick} />,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies ConsoleFeatureUiRegistry;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getPatchBaseFileHash, getPatchFileHash } from "@hot-updater/core";
|
|
2
1
|
import type { Bundle, ReleaseRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import { getPatchBaseFileHash, getPatchFileHash } from "@hot-updater/protocol";
|
|
3
3
|
import { ExternalLink } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { HashValueDisplay } from "@/components/HashValueDisplay";
|
|
@@ -36,7 +36,7 @@ export function BundleMetadata({ bundle, release }: BundleMetadataProps) {
|
|
|
36
36
|
const hasMetadata =
|
|
37
37
|
target ||
|
|
38
38
|
bundle?.gitCommitHash ||
|
|
39
|
-
bundle?.
|
|
39
|
+
bundle?.manifestFileHash ||
|
|
40
40
|
hbcPatchBaseFileHash ||
|
|
41
41
|
hbcPatchFileHash;
|
|
42
42
|
|
|
@@ -86,11 +86,14 @@ export function BundleMetadata({ bundle, release }: BundleMetadataProps) {
|
|
|
86
86
|
}
|
|
87
87
|
/>
|
|
88
88
|
) : null}
|
|
89
|
-
{bundle?.
|
|
89
|
+
{bundle?.manifestFileHash ? (
|
|
90
90
|
<Row
|
|
91
|
-
label="
|
|
91
|
+
label="Manifest hash"
|
|
92
92
|
value={
|
|
93
|
-
<HashValueDisplay
|
|
93
|
+
<HashValueDisplay
|
|
94
|
+
maxLength={16}
|
|
95
|
+
value={bundle.manifestFileHash}
|
|
96
|
+
/>
|
|
94
97
|
}
|
|
95
98
|
/>
|
|
96
99
|
) : null}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChannelRow } from "@hot-updater/plugin-core";
|
|
2
2
|
import { Loader2, Plus, Tag, Trash2 } from "lucide-react";
|
|
3
3
|
import { type FormEvent, useState } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
@@ -55,10 +55,7 @@ export function ChannelManagementDialog({
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
|
-
const result = await createChannel.mutateAsync({
|
|
59
|
-
row: { id: createUUIDv7(), name: normalizedName },
|
|
60
|
-
onConflict: "returnExisting",
|
|
61
|
-
});
|
|
58
|
+
const result = await createChannel.mutateAsync({ name: normalizedName });
|
|
62
59
|
setChannelName("");
|
|
63
60
|
if (result.inserted) {
|
|
64
61
|
toast.success(`Channel ${result.row.name} created`);
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import { Link } from "@tanstack/react-router";
|
|
3
|
+
import { ArrowRight, ChevronRight } from "lucide-react";
|
|
4
|
+
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
|
5
|
+
|
|
6
|
+
import { PlatformIcon } from "@/components/PlatformIcon";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
9
|
+
import {
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardFooter,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
} from "@/components/ui/card";
|
|
16
|
+
import {
|
|
17
|
+
ChartContainer,
|
|
18
|
+
ChartTooltip,
|
|
19
|
+
ChartTooltipContent,
|
|
20
|
+
} from "@/components/ui/chart";
|
|
21
|
+
import {
|
|
22
|
+
Empty,
|
|
23
|
+
EmptyDescription,
|
|
24
|
+
EmptyHeader,
|
|
25
|
+
EmptyTitle,
|
|
26
|
+
} from "@/components/ui/empty";
|
|
27
|
+
import { Progress } from "@/components/ui/progress";
|
|
28
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
29
|
+
import { Separator } from "@/components/ui/separator";
|
|
30
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
31
|
+
import {
|
|
32
|
+
Table,
|
|
33
|
+
TableBody,
|
|
34
|
+
TableCell,
|
|
35
|
+
TableHead,
|
|
36
|
+
TableHeader,
|
|
37
|
+
TableRow,
|
|
38
|
+
} from "@/components/ui/table";
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_INSIGHTS_RETENTION,
|
|
41
|
+
type InsightsRetentionDays,
|
|
42
|
+
keepsTwelveMonths,
|
|
43
|
+
} from "@/lib/insights-retention";
|
|
44
|
+
import type { InsightsSearch } from "@/lib/insights-search";
|
|
45
|
+
import type { AppUsageReport, UsageWindow } from "@/lib/insights-usage";
|
|
46
|
+
|
|
47
|
+
import { EstimatedCount } from "./EstimatedCount";
|
|
48
|
+
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
49
|
+
import { InsightsInfo } from "./InsightsInfo";
|
|
50
|
+
import { UsagePeriodSelector } from "./InsightsPeriodSelector";
|
|
51
|
+
import { ReportingDevicesSummary } from "./ReportingDevicesSummary";
|
|
52
|
+
|
|
53
|
+
const dates = new Intl.DateTimeFormat("en", {
|
|
54
|
+
month: "short",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
timeZone: "UTC",
|
|
57
|
+
});
|
|
58
|
+
const times = new Intl.DateTimeFormat("en", {
|
|
59
|
+
month: "short",
|
|
60
|
+
day: "numeric",
|
|
61
|
+
hour: "numeric",
|
|
62
|
+
timeZone: "UTC",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export function AppUsage({
|
|
66
|
+
query,
|
|
67
|
+
search,
|
|
68
|
+
window,
|
|
69
|
+
onWindowChange,
|
|
70
|
+
retention = DEFAULT_INSIGHTS_RETENTION,
|
|
71
|
+
}: {
|
|
72
|
+
readonly query: UseQueryResult<AppUsageReport>;
|
|
73
|
+
/** How long the server keeps Insights rows. */
|
|
74
|
+
readonly retention?: InsightsRetentionDays;
|
|
75
|
+
readonly search: InsightsSearch;
|
|
76
|
+
readonly window: UsageWindow;
|
|
77
|
+
readonly onWindowChange: (window: UsageWindow) => void;
|
|
78
|
+
}) {
|
|
79
|
+
const report = query.error ? undefined : query.data;
|
|
80
|
+
const formatTime = (ms: number) =>
|
|
81
|
+
(window === "30d" || window === "12m" ? dates : times).format(ms);
|
|
82
|
+
const distributionPeriod = report
|
|
83
|
+
? `since ${dates.format(report.distributionSinceMs)}, 00:00 UTC`
|
|
84
|
+
: "in the selected period";
|
|
85
|
+
const distributionTotals = report
|
|
86
|
+
? {
|
|
87
|
+
versions: report.versions.reduce(
|
|
88
|
+
(sum, row) => sum + row.installations,
|
|
89
|
+
0,
|
|
90
|
+
),
|
|
91
|
+
platforms: report.platforms.reduce(
|
|
92
|
+
(sum, row) => sum + row.installations,
|
|
93
|
+
0,
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
: undefined;
|
|
97
|
+
return (
|
|
98
|
+
<div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(16rem,1fr)]">
|
|
99
|
+
<Card aria-label="App usage" role="region" className="min-w-0 shadow-sm">
|
|
100
|
+
<CardHeader className="flex-row flex-wrap items-center justify-between gap-4">
|
|
101
|
+
<CardTitle>App usage</CardTitle>
|
|
102
|
+
<UsagePeriodSelector
|
|
103
|
+
window={window}
|
|
104
|
+
onWindowChange={onWindowChange}
|
|
105
|
+
twelveMonths={keepsTwelveMonths(retention)}
|
|
106
|
+
/>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent className="flex flex-col gap-4">
|
|
109
|
+
<ReportingDevicesSummary
|
|
110
|
+
window={window}
|
|
111
|
+
count={report?.activeInstallations}
|
|
112
|
+
isPending={query.isPending}
|
|
113
|
+
partial={report?.truncated ?? false}
|
|
114
|
+
retention={retention}
|
|
115
|
+
/>
|
|
116
|
+
{query.isPending ? (
|
|
117
|
+
<Skeleton aria-label="Loading app usage" className="h-48" />
|
|
118
|
+
) : query.error ? (
|
|
119
|
+
<div className="flex flex-col items-start gap-4">
|
|
120
|
+
<InsightsErrorAlert
|
|
121
|
+
error={query.error}
|
|
122
|
+
fallbackTitle="App usage unavailable"
|
|
123
|
+
/>
|
|
124
|
+
<Button variant="outline" onClick={() => void query.refetch()}>
|
|
125
|
+
Retry app usage
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
) : report?.activeInstallations === 0 ? (
|
|
129
|
+
<Empty className="h-48">
|
|
130
|
+
<EmptyHeader>
|
|
131
|
+
<EmptyTitle>No activity reported</EmptyTitle>
|
|
132
|
+
<EmptyDescription>
|
|
133
|
+
Try another period, platform, channel, or app version.
|
|
134
|
+
</EmptyDescription>
|
|
135
|
+
</EmptyHeader>
|
|
136
|
+
</Empty>
|
|
137
|
+
) : report ? (
|
|
138
|
+
<>
|
|
139
|
+
<div>
|
|
140
|
+
<p className="mb-2 text-right text-xs text-muted-foreground">
|
|
141
|
+
Reporting installations · UTC
|
|
142
|
+
</p>
|
|
143
|
+
<ChartContainer
|
|
144
|
+
aria-label="Active users per interval"
|
|
145
|
+
className="h-48 w-full aspect-auto"
|
|
146
|
+
config={{
|
|
147
|
+
installations: {
|
|
148
|
+
label: "Active users",
|
|
149
|
+
color: "var(--chart-2)",
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<LineChart
|
|
154
|
+
accessibilityLayer
|
|
155
|
+
data={report.points}
|
|
156
|
+
margin={{ right: 8, top: 8 }}
|
|
157
|
+
>
|
|
158
|
+
<CartesianGrid vertical={false} />
|
|
159
|
+
<XAxis
|
|
160
|
+
dataKey="startMs"
|
|
161
|
+
tick={{ fill: "var(--muted-foreground)" }}
|
|
162
|
+
tickFormatter={formatTime}
|
|
163
|
+
axisLine={false}
|
|
164
|
+
tickLine={false}
|
|
165
|
+
minTickGap={45}
|
|
166
|
+
/>
|
|
167
|
+
<YAxis
|
|
168
|
+
tick={{ fill: "var(--muted-foreground)" }}
|
|
169
|
+
allowDecimals={false}
|
|
170
|
+
axisLine={false}
|
|
171
|
+
tickLine={false}
|
|
172
|
+
width="auto"
|
|
173
|
+
/>
|
|
174
|
+
<ChartTooltip
|
|
175
|
+
content={
|
|
176
|
+
<ChartTooltipContent
|
|
177
|
+
labelFormatter={(_, payload) =>
|
|
178
|
+
`${times.format(payload[0]?.payload.startMs)} · UTC`
|
|
179
|
+
}
|
|
180
|
+
/>
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
<Line
|
|
184
|
+
dataKey="installations"
|
|
185
|
+
type="linear"
|
|
186
|
+
stroke="var(--color-installations)"
|
|
187
|
+
strokeWidth={2}
|
|
188
|
+
dot={{ r: 2 }}
|
|
189
|
+
activeDot={{ r: 4 }}
|
|
190
|
+
isAnimationActive={false}
|
|
191
|
+
/>
|
|
192
|
+
</LineChart>
|
|
193
|
+
</ChartContainer>
|
|
194
|
+
</div>
|
|
195
|
+
<details className="text-xs text-muted-foreground">
|
|
196
|
+
<summary className="cursor-pointer py-2">
|
|
197
|
+
View usage data
|
|
198
|
+
</summary>
|
|
199
|
+
<Table aria-label="App usage by interval">
|
|
200
|
+
<TableHeader>
|
|
201
|
+
<TableRow>
|
|
202
|
+
<TableHead>Interval · UTC</TableHead>
|
|
203
|
+
<TableHead className="text-right">Active users</TableHead>
|
|
204
|
+
</TableRow>
|
|
205
|
+
</TableHeader>
|
|
206
|
+
<TableBody>
|
|
207
|
+
{report.points.map((point) => (
|
|
208
|
+
<TableRow key={point.startMs}>
|
|
209
|
+
<TableCell>{times.format(point.startMs)}</TableCell>
|
|
210
|
+
<TableCell className="text-right tabular-nums">
|
|
211
|
+
{point.installations === null ? (
|
|
212
|
+
"Unknown"
|
|
213
|
+
) : (
|
|
214
|
+
<EstimatedCount value={point.installations} />
|
|
215
|
+
)}
|
|
216
|
+
</TableCell>
|
|
217
|
+
</TableRow>
|
|
218
|
+
))}
|
|
219
|
+
</TableBody>
|
|
220
|
+
</Table>
|
|
221
|
+
</details>
|
|
222
|
+
</>
|
|
223
|
+
) : null}
|
|
224
|
+
</CardContent>
|
|
225
|
+
</Card>
|
|
226
|
+
<Card
|
|
227
|
+
aria-label="Usage distribution"
|
|
228
|
+
role="region"
|
|
229
|
+
className="flex min-w-0 flex-col shadow-sm"
|
|
230
|
+
>
|
|
231
|
+
<CardHeader className="flex-row items-center justify-between gap-3">
|
|
232
|
+
<CardTitle className="flex min-h-9 items-center">
|
|
233
|
+
Distribution
|
|
234
|
+
</CardTitle>
|
|
235
|
+
<InsightsInfo label="How distribution is counted">
|
|
236
|
+
Each reporting installation is counted once, using the app version
|
|
237
|
+
and platform of its latest matching report {distributionPeriod}.
|
|
238
|
+
Latest reports are counted by UTC day.
|
|
239
|
+
{report?.truncated
|
|
240
|
+
? " Shares reflect only the available history."
|
|
241
|
+
: ""}
|
|
242
|
+
</InsightsInfo>
|
|
243
|
+
</CardHeader>
|
|
244
|
+
<CardContent className="flex-1">
|
|
245
|
+
{query.isPending ? (
|
|
246
|
+
<Skeleton aria-label="Loading distribution" className="h-48" />
|
|
247
|
+
) : query.error ? (
|
|
248
|
+
<InsightsErrorAlert
|
|
249
|
+
error={query.error}
|
|
250
|
+
fallbackTitle="Distribution unavailable"
|
|
251
|
+
/>
|
|
252
|
+
) : !report?.activeInstallations ? (
|
|
253
|
+
<Empty className="h-40">
|
|
254
|
+
<EmptyHeader>
|
|
255
|
+
<EmptyTitle>No reporting installations</EmptyTitle>
|
|
256
|
+
<EmptyDescription>
|
|
257
|
+
Try a different filter or period.
|
|
258
|
+
</EmptyDescription>
|
|
259
|
+
</EmptyHeader>
|
|
260
|
+
</Empty>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="flex flex-col gap-6">
|
|
263
|
+
{(["versions", "platforms"] as const).map((dimension) => (
|
|
264
|
+
<section
|
|
265
|
+
key={dimension}
|
|
266
|
+
aria-label={
|
|
267
|
+
dimension === "versions"
|
|
268
|
+
? "App version distribution"
|
|
269
|
+
: "Platform distribution"
|
|
270
|
+
}
|
|
271
|
+
className="flex flex-col gap-4"
|
|
272
|
+
>
|
|
273
|
+
{dimension === "platforms" ? <Separator /> : null}
|
|
274
|
+
<h3 className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
275
|
+
{dimension === "versions" ? "App versions" : "Platforms"}
|
|
276
|
+
{dimension === "versions" ? (
|
|
277
|
+
<Badge variant="secondary">
|
|
278
|
+
{report.versions.length}
|
|
279
|
+
</Badge>
|
|
280
|
+
) : null}
|
|
281
|
+
</h3>
|
|
282
|
+
<ScrollArea className="min-w-0 [&_[data-slot=scroll-area-viewport]]:max-h-40">
|
|
283
|
+
<dl className="flex flex-col gap-4 pr-4">
|
|
284
|
+
{report[dimension].map((row) => {
|
|
285
|
+
const name =
|
|
286
|
+
dimension === "platforms"
|
|
287
|
+
? row.name === "ios"
|
|
288
|
+
? "iOS"
|
|
289
|
+
: "Android"
|
|
290
|
+
: row.name;
|
|
291
|
+
const share =
|
|
292
|
+
(row.installations /
|
|
293
|
+
(distributionTotals?.[dimension] ?? 1)) *
|
|
294
|
+
100;
|
|
295
|
+
return (
|
|
296
|
+
<div
|
|
297
|
+
key={row.name}
|
|
298
|
+
className="group relative flex min-h-11 flex-col gap-2 rounded-sm focus-within:ring-2 focus-within:ring-ring sm:min-h-0"
|
|
299
|
+
>
|
|
300
|
+
<div className="flex min-w-0 items-baseline justify-between gap-3">
|
|
301
|
+
<dt
|
|
302
|
+
className="flex min-w-0 items-center gap-2 text-sm"
|
|
303
|
+
title={name}
|
|
304
|
+
>
|
|
305
|
+
{dimension === "platforms" &&
|
|
306
|
+
(row.name === "ios" ||
|
|
307
|
+
row.name === "android") ? (
|
|
308
|
+
<PlatformIcon
|
|
309
|
+
platform={row.name}
|
|
310
|
+
className="size-4 shrink-0"
|
|
311
|
+
/>
|
|
312
|
+
) : null}
|
|
313
|
+
{dimension === "versions" ? (
|
|
314
|
+
<Link
|
|
315
|
+
to="/insights/distribution"
|
|
316
|
+
search={{ ...search, version: row.name }}
|
|
317
|
+
aria-label={`View bundles for app version ${row.name}`}
|
|
318
|
+
className="flex min-w-0 items-center gap-1 after:absolute after:inset-0 group-hover:underline"
|
|
319
|
+
>
|
|
320
|
+
<span className="truncate">{name}</span>
|
|
321
|
+
<ChevronRight
|
|
322
|
+
aria-hidden="true"
|
|
323
|
+
className="size-3 shrink-0 text-muted-foreground"
|
|
324
|
+
/>
|
|
325
|
+
</Link>
|
|
326
|
+
) : (
|
|
327
|
+
<span className="truncate">{name}</span>
|
|
328
|
+
)}
|
|
329
|
+
</dt>
|
|
330
|
+
<dd className="flex shrink-0 gap-3 text-sm tabular-nums">
|
|
331
|
+
<span>
|
|
332
|
+
{row.installations.toLocaleString()}
|
|
333
|
+
</span>
|
|
334
|
+
<span className="min-w-10 text-right text-muted-foreground">
|
|
335
|
+
{share.toFixed(1)}%
|
|
336
|
+
</span>
|
|
337
|
+
</dd>
|
|
338
|
+
</div>
|
|
339
|
+
<Progress
|
|
340
|
+
aria-label={`${name} share`}
|
|
341
|
+
aria-valuetext={`${row.installations} installations, ${share.toFixed(1)}%`}
|
|
342
|
+
value={share}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
})}
|
|
347
|
+
</dl>
|
|
348
|
+
</ScrollArea>
|
|
349
|
+
</section>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</CardContent>
|
|
354
|
+
<CardFooter className="border-t pt-4">
|
|
355
|
+
<Link
|
|
356
|
+
to="/insights/distribution"
|
|
357
|
+
search={search}
|
|
358
|
+
className={buttonVariants({
|
|
359
|
+
variant: "outline",
|
|
360
|
+
className: "h-11 w-full sm:h-9",
|
|
361
|
+
})}
|
|
362
|
+
>
|
|
363
|
+
View distribution
|
|
364
|
+
<ArrowRight aria-hidden="true" data-icon="inline-end" />
|
|
365
|
+
</Link>
|
|
366
|
+
</CardFooter>
|
|
367
|
+
</Card>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unique count the server estimates from a HyperLogLog sketch, typically
|
|
3
|
+
* within about 3% of the true count. It shows as approximate, as analytics
|
|
4
|
+
* consoles mark estimates, and says so to screen readers. Zero is exact: no
|
|
5
|
+
* report reached the sketch.
|
|
6
|
+
*/
|
|
7
|
+
export function EstimatedCount({ value }: { readonly value: number }) {
|
|
8
|
+
if (value === 0) return "0";
|
|
9
|
+
return (
|
|
10
|
+
<span title="Estimated">
|
|
11
|
+
<span aria-hidden="true" className="mr-1 text-muted-foreground">
|
|
12
|
+
≈
|
|
13
|
+
</span>
|
|
14
|
+
<span className="sr-only">Estimated </span>
|
|
15
|
+
{value.toLocaleString()}
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|