@hot-updater/console 1.0.0-rc.9 → 1.0.0
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-jB4Loglh.js +1 -0
- package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
- package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
- package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
- package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
- package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
- package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
- package/.output/public/assets/api-DCM0wr6O.js +1 -0
- package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
- package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
- package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
- package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
- package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
- package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
- package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
- package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
- package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
- package/.output/public/assets/empty-CuT2UhFp.js +1 -0
- package/.output/public/assets/field-BBDpjIqu.js +1 -0
- package/.output/public/assets/index-6o51GkFe.js +11 -0
- package/.output/public/assets/input-group-ja_30qaF.js +1 -0
- package/.output/public/assets/insights-CmraDzQh.js +1 -0
- package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
- package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
- package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
- package/.output/public/assets/insights-vNRCrfnS.js +42 -0
- package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
- package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
- package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
- package/.output/public/assets/installations-BazSJXE0.js +2 -0
- package/.output/public/assets/installations-ClHTqdAO.js +1 -0
- package/.output/public/assets/installations-DpKsBuHi.js +1 -0
- package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
- package/.output/public/assets/list-CgrYiadM.js +1 -0
- package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
- package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
- package/.output/public/assets/remote-config-Br56BODb.js +6 -0
- package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
- package/.output/public/assets/routes-B88Erkpk.js +2 -0
- package/.output/public/assets/routes-CfzsAleC.js +1 -0
- package/.output/public/assets/search-DaTDPW25.js +1 -0
- package/.output/public/assets/select-BZZBXMkr.js +1 -0
- package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
- package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
- package/.output/public/assets/styles-DTGEAJFv.css +2 -0
- package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
- package/.output/public/assets/table-Gm4qRWwE.js +1 -0
- package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
- package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
- package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
- package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
- 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 +221 -9
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +1 -1
- package/.output/server/_libs/recharts+[...].mjs +483 -901
- 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 +2010 -309
- package/.output/server/_libs/tanstack__react-query.mjs +448 -21
- package/.output/server/_libs/verkit.mjs +102 -1
- package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
- package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
- package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
- package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
- package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
- package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
- package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
- package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
- package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
- package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
- package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
- package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
- package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
- package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
- package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
- package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
- package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
- package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
- package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
- package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
- package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
- package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
- package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
- package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
- package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
- package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
- package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
- package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
- package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
- package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
- package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
- package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
- package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
- package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
- package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
- package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
- package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
- package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
- package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
- package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
- package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
- package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
- package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
- package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
- package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
- package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
- package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
- package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
- package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
- package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
- package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
- package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
- package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
- package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
- package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
- package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
- package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
- package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
- package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
- package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
- package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
- package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
- package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
- package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.mjs} +107 -47
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
- package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
- package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
- package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
- package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
- package/.output/server/index.mjs +309 -211
- package/LICENSE +13 -14
- package/README.md +80 -71
- 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 +69 -74
- package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +103 -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 +8 -6
- package/src/components/features/insights/AppUsage.tsx +41 -14
- package/src/components/features/insights/BuiltinBundle.tsx +36 -0
- package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +390 -26
- package/src/components/features/insights/EventHistoryCard.tsx +111 -25
- package/src/components/features/insights/EventHistoryList.tsx +1 -1
- package/src/components/features/insights/FailureReports.tsx +407 -0
- package/src/components/features/insights/InsightsControls.tsx +240 -142
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsOverview.tsx +601 -403
- package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
- package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
- package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +308 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
- package/src/components/features/insights/RollbackButton.tsx +118 -0
- package/src/components/features/insights/UpdateFailures.tsx +382 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
- package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
- package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
- package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
- package/src/components/features/remote-config/ParametersCard.tsx +254 -0
- package/src/components/features/remote-config/PreviewCard.tsx +284 -0
- package/src/components/features/remote-config/PublishDialog.tsx +325 -0
- package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
- package/src/components/features/remote-config/RuleSummary.tsx +21 -0
- package/src/components/features/remote-config/ValueInput.tsx +151 -0
- package/src/components/features/remote-config/VersionsCard.tsx +384 -0
- package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
- package/src/components/features/remote-config/useDateTimeText.ts +16 -0
- package/src/components/ui/sidebar.tsx +22 -29
- 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 +125 -288
- package/src/lib/api.ts +15 -134
- package/src/lib/bundle-activity.ts +24 -4
- 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 +98 -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 +130 -0
- package/src/lib/insights-recovery-rpc.ts +13 -17
- package/src/lib/insights-recovery.ts +12 -36
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +50 -4
- package/src/lib/insights-usage-rpc.ts +15 -9
- package/src/lib/insights-usage.ts +41 -3
- package/src/lib/insights-view.ts +34 -5
- package/src/lib/release-adoption-api.ts +214 -0
- package/src/lib/release-adoption-rpc.ts +49 -0
- package/src/lib/release-adoption.ts +164 -0
- package/src/lib/remote-config-api.ts +119 -0
- package/src/lib/remote-config-draft.ts +666 -0
- package/src/lib/remote-config-rpc.ts +196 -0
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +27 -28
- package/src/lib/server/config.server.ts +21 -76
- package/src/lib/server/console-feature-apis.ts +70 -0
- package/src/lib/server/console-runtime.server.ts +82 -38
- package/src/lib/server/downloadBundle.ts +14 -15
- package/src/lib/server/failureReports.ts +50 -0
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsUsage.ts +52 -113
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseAdoption.ts +151 -0
- package/src/lib/server/releaseReachability.ts +8 -8
- package/src/lib/server/remoteConfig.ts +75 -0
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +66 -0
- package/src/lib/use-dialog-target.ts +27 -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 +7 -1
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +210 -152
- package/src/routes/insights.tsx +91 -23
- package/src/routes/insights_.distribution.tsx +50 -9
- package/src/routes/installations.tsx +97 -40
- package/src/routes/remote-config.tsx +42 -0
- package/src/styles.css +9 -0
- package/src/vite-internal.ts +15 -3
- package/src/vite.ts +2 -0
- package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
- package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
- package/.output/public/assets/api-CdUyUqzR.js +0 -42
- package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
- package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
- package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
- package/.output/public/assets/card-CYHs5FZv.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
- package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
- package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
- package/.output/public/assets/dist-BR4GzeEu.js +0 -1
- package/.output/public/assets/field-B3CYDpjg.js +0 -1
- package/.output/public/assets/index-Cb_D1D6N.js +0 -10
- package/.output/public/assets/info-C3y9HNoU.js +0 -1
- package/.output/public/assets/input-group-nLZylhWF.js +0 -1
- package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
- package/.output/public/assets/insights-DS3iO7Up.js +0 -1
- package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
- package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
- package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
- package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
- package/.output/public/assets/installations-CkFW4fV9.js +0 -2
- package/.output/public/assets/installations-DxTaIcHD.js +0 -1
- package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
- package/.output/public/assets/list-Dqt53fWZ.js +0 -1
- package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
- package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
- package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
- package/.output/public/assets/routes-Cgtvspjg.js +0 -2
- package/.output/public/assets/routes-O2r2UUN8.js +0 -1
- package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
- package/.output/public/assets/signing-BgspC-xP.js +0 -1
- package/.output/public/assets/styles-DnrWuBwd.css +0 -2
- package/.output/public/assets/table-BuWQgxJJ.js +0 -1
- package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
- package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
- package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
- package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
- package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
- package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
- package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
- package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
- package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
- package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
- package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
- package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
- package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
- package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
- package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
- package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
- package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
- package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
- package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
- package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
- package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
- package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
- package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
- package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
- package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
- package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
- package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
- package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
- package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
- package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
- package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
- 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/insightsHistory.ts +0 -52
- package/src/lib/server/insightsRecovery.ts +0 -187
- package/src/routes/signing.tsx +0 -37
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Link } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { ListIcon, RotateCcw, RotateCw, TriangleAlert, X } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
|
5
4
|
|
|
5
|
+
import { BundleIdDisplay } from "@/components/BundleIdDisplay";
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
8
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
7
9
|
import {
|
|
8
10
|
Card,
|
|
@@ -12,432 +14,628 @@ import {
|
|
|
12
14
|
CardTitle,
|
|
13
15
|
} from "@/components/ui/card";
|
|
14
16
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Empty,
|
|
18
|
+
EmptyContent,
|
|
19
|
+
EmptyDescription,
|
|
20
|
+
EmptyHeader,
|
|
21
|
+
EmptyTitle,
|
|
22
|
+
} from "@/components/ui/empty";
|
|
23
|
+
import {
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectGroup,
|
|
27
|
+
SelectItem,
|
|
28
|
+
SelectTrigger,
|
|
29
|
+
SelectValue,
|
|
30
|
+
} from "@/components/ui/select";
|
|
20
31
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
32
|
+
import {
|
|
33
|
+
Table,
|
|
34
|
+
TableBody,
|
|
35
|
+
TableCell,
|
|
36
|
+
TableHead,
|
|
37
|
+
TableHeader,
|
|
38
|
+
TableRow,
|
|
39
|
+
} from "@/components/ui/table";
|
|
21
40
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
22
|
-
import type { RecoveryInput
|
|
41
|
+
import type { RecoveryInput } from "@/lib/insights-recovery";
|
|
42
|
+
import type { HealthChart } from "@/lib/insights-search";
|
|
43
|
+
import {
|
|
44
|
+
CRASH_MIN_ATTEMPTS,
|
|
45
|
+
CRASH_RATE_THRESHOLD,
|
|
46
|
+
crashRateOf,
|
|
47
|
+
DEFAULT_ADOPTION_RELEASES,
|
|
48
|
+
MAX_ADOPTION_RELEASES,
|
|
49
|
+
recommendsRollback,
|
|
50
|
+
} from "@/lib/release-adoption";
|
|
51
|
+
import type {
|
|
52
|
+
ComparedBundle,
|
|
53
|
+
ReleaseHealthState,
|
|
54
|
+
} from "@/lib/release-adoption-api";
|
|
23
55
|
import { cn } from "@/lib/utils";
|
|
24
56
|
|
|
57
|
+
import {
|
|
58
|
+
ComparedBundlesChart,
|
|
59
|
+
formatDeployedAt,
|
|
60
|
+
useSeriesColors,
|
|
61
|
+
} from "./ComparedBundlesChart";
|
|
25
62
|
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
63
|
+
import { InsightsInfo } from "./InsightsInfo";
|
|
26
64
|
import { InsightsPeriodSelector } from "./InsightsPeriodSelector";
|
|
65
|
+
import { RollbackButton } from "./RollbackButton";
|
|
27
66
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const colors = [
|
|
40
|
-
"var(--chart-2)",
|
|
41
|
-
"var(--foreground)",
|
|
42
|
-
"var(--chart-1)",
|
|
43
|
-
"var(--muted-foreground)",
|
|
44
|
-
"var(--chart-4)",
|
|
45
|
-
];
|
|
46
|
-
const dashes = [undefined, "6 3", "2 3"];
|
|
67
|
+
const HOUR = 3_600_000;
|
|
68
|
+
const DAY = 24 * HOUR;
|
|
69
|
+
const percent = (rate: number) => `${(rate * 100).toFixed(1)}%`;
|
|
70
|
+
const lengthOf = (durationMs: number) =>
|
|
71
|
+
durationMs > DAY ? `${durationMs / DAY} days` : `${durationMs / HOUR} hours`;
|
|
72
|
+
const cadenceOf = (intervalMs: number) =>
|
|
73
|
+
intervalMs === HOUR
|
|
74
|
+
? "Hourly"
|
|
75
|
+
: intervalMs === DAY
|
|
76
|
+
? "Daily"
|
|
77
|
+
: `Every ${intervalMs / HOUR} hours`;
|
|
47
78
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
startMs: point.startMs,
|
|
65
|
-
...Object.fromEntries(
|
|
66
|
-
report.series.map((series, index) => [
|
|
67
|
-
`bundle${index}`,
|
|
68
|
-
metric === "active"
|
|
69
|
-
? series.points[pointIndex].active
|
|
70
|
-
: series.points[pointIndex].recoveredInstallations,
|
|
71
|
-
]),
|
|
72
|
-
),
|
|
73
|
-
}));
|
|
74
|
-
const formatTick = (ms: number) =>
|
|
75
|
-
(report.intervalMs < 86_400_000 ? times : dates).format(ms);
|
|
76
|
-
const lastSpike = selected
|
|
77
|
-
? [...selected.points].reverse().find((point) => point.spike)
|
|
78
|
-
: undefined;
|
|
79
|
-
const total =
|
|
80
|
-
metric === "active"
|
|
81
|
-
? report.series.reduce(
|
|
82
|
-
(sum, series) => sum + series.activeInstallations,
|
|
83
|
-
0,
|
|
84
|
-
)
|
|
85
|
-
: report.series.filter((series) => series.recoveredInstallations > 0)
|
|
86
|
-
.length;
|
|
79
|
+
/** A bundle's crash numbers over the period, once both counts are read. */
|
|
80
|
+
const crashesOf = ({ release, launched, recovered }: ComparedBundle) =>
|
|
81
|
+
launched === undefined || recovered === undefined
|
|
82
|
+
? undefined
|
|
83
|
+
: {
|
|
84
|
+
crashes: recovered.total,
|
|
85
|
+
...crashRateOf(launched.total, recovered.total),
|
|
86
|
+
recommended: recommendsRollback(
|
|
87
|
+
release,
|
|
88
|
+
launched.total,
|
|
89
|
+
recovered.total,
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A line sample for the legend: solid or dashed, in the text color. */
|
|
94
|
+
function LineSample({ dashed }: { readonly dashed?: boolean }) {
|
|
87
95
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
className="gap-6"
|
|
96
|
+
<svg
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
className="h-3 w-6 shrink-0"
|
|
99
|
+
viewBox="0 0 24 12"
|
|
100
|
+
preserveAspectRatio="none"
|
|
94
101
|
>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</TabsTrigger>
|
|
106
|
-
</TabsList>
|
|
107
|
-
<TabsContent value={metric} className="flex flex-col gap-4">
|
|
108
|
-
<div className="flex items-baseline gap-3">
|
|
109
|
-
<p className="text-4xl font-semibold tracking-tight tabular-nums">
|
|
110
|
-
{total.toLocaleString()}
|
|
111
|
-
</p>
|
|
112
|
-
<p className="text-sm text-muted-foreground">
|
|
113
|
-
{metric === "active"
|
|
114
|
-
? "Active installations"
|
|
115
|
-
: "Bundles with rollbacks"}
|
|
116
|
-
</p>
|
|
117
|
-
</div>
|
|
118
|
-
{report.series.length === 0 ? (
|
|
119
|
-
<div className="flex h-48 items-center justify-center text-center text-sm text-muted-foreground">
|
|
120
|
-
No bundle reports in this period. Try another channel or check again
|
|
121
|
-
after the app reports activity.
|
|
122
|
-
</div>
|
|
123
|
-
) : (
|
|
124
|
-
<>
|
|
125
|
-
<div>
|
|
126
|
-
<p className="mb-2 text-right text-xs text-muted-foreground">
|
|
127
|
-
{metric === "active"
|
|
128
|
-
? "Installations"
|
|
129
|
-
: "Recovered installations per interval"}{" "}
|
|
130
|
-
· UTC
|
|
131
|
-
</p>
|
|
132
|
-
<ChartContainer
|
|
133
|
-
aria-label={`${metric === "active" ? "Active" : "Rollback"} trend for all reported bundle IDs`}
|
|
134
|
-
className="h-56 w-full aspect-auto sm:h-64"
|
|
135
|
-
config={config}
|
|
136
|
-
>
|
|
137
|
-
<LineChart
|
|
138
|
-
accessibilityLayer
|
|
139
|
-
data={data}
|
|
140
|
-
margin={{ left: -12, right: 12, top: 8 }}
|
|
141
|
-
>
|
|
142
|
-
<CartesianGrid vertical={false} />
|
|
143
|
-
<XAxis
|
|
144
|
-
tick={{ fill: "var(--muted-foreground)" }}
|
|
145
|
-
axisLine={false}
|
|
146
|
-
dataKey="startMs"
|
|
147
|
-
minTickGap={40}
|
|
148
|
-
tickFormatter={formatTick}
|
|
149
|
-
tickLine={false}
|
|
150
|
-
/>
|
|
151
|
-
<YAxis
|
|
152
|
-
tick={{ fill: "var(--muted-foreground)" }}
|
|
153
|
-
allowDecimals={false}
|
|
154
|
-
axisLine={false}
|
|
155
|
-
tickLine={false}
|
|
156
|
-
width={48}
|
|
157
|
-
/>
|
|
158
|
-
<ChartTooltip
|
|
159
|
-
content={
|
|
160
|
-
<ChartTooltipContent
|
|
161
|
-
className="max-h-64 max-w-[calc(100vw-3rem)] overflow-auto"
|
|
162
|
-
labelFormatter={(_, payload) =>
|
|
163
|
-
`${times.format(payload[0]?.payload.startMs)} · UTC`
|
|
164
|
-
}
|
|
165
|
-
/>
|
|
166
|
-
}
|
|
167
|
-
/>
|
|
168
|
-
{report.series.map((series, index) => (
|
|
169
|
-
<Line
|
|
170
|
-
key={series.releaseId}
|
|
171
|
-
dataKey={`bundle${index}`}
|
|
172
|
-
type="linear"
|
|
173
|
-
stroke={`var(--color-bundle${index})`}
|
|
174
|
-
strokeWidth={selectedId === series.releaseId ? 3 : 2}
|
|
175
|
-
strokeOpacity={!selected || selected === series ? 1 : 0.4}
|
|
176
|
-
strokeDasharray={
|
|
177
|
-
dashes[
|
|
178
|
-
Math.floor(index / colors.length) % dashes.length
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
dot={{ r: 2 }}
|
|
182
|
-
activeDot={{ r: 4 }}
|
|
183
|
-
isAnimationActive={false}
|
|
184
|
-
/>
|
|
185
|
-
))}
|
|
186
|
-
</LineChart>
|
|
187
|
-
</ChartContainer>
|
|
188
|
-
</div>
|
|
189
|
-
<ScrollArea
|
|
190
|
-
aria-label="Reported bundles"
|
|
191
|
-
className="min-w-0 [&_[data-slot=scroll-area-viewport]]:max-h-48"
|
|
192
|
-
>
|
|
193
|
-
<div
|
|
194
|
-
className={cn(
|
|
195
|
-
"grid grid-cols-1 gap-1 pr-3",
|
|
196
|
-
report.series.length > 1 && "xl:grid-cols-2",
|
|
197
|
-
)}
|
|
198
|
-
>
|
|
199
|
-
{report.series.map((series, index) => (
|
|
200
|
-
<button
|
|
201
|
-
type="button"
|
|
202
|
-
key={series.releaseId}
|
|
203
|
-
aria-label={`Highlight bundle ${series.releaseId}`}
|
|
204
|
-
aria-pressed={selectedId === series.releaseId}
|
|
205
|
-
title={series.releaseId}
|
|
206
|
-
onClick={() =>
|
|
207
|
-
setSelectedId(
|
|
208
|
-
selectedId === series.releaseId
|
|
209
|
-
? null
|
|
210
|
-
: series.releaseId,
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-muted"
|
|
214
|
-
>
|
|
215
|
-
<svg
|
|
216
|
-
aria-hidden="true"
|
|
217
|
-
width="20"
|
|
218
|
-
height="8"
|
|
219
|
-
className="shrink-0"
|
|
220
|
-
>
|
|
221
|
-
<line
|
|
222
|
-
x1="0"
|
|
223
|
-
x2="20"
|
|
224
|
-
y1="4"
|
|
225
|
-
y2="4"
|
|
226
|
-
stroke={colors[index % colors.length]}
|
|
227
|
-
strokeWidth="2"
|
|
228
|
-
strokeDasharray={
|
|
229
|
-
dashes[
|
|
230
|
-
Math.floor(index / colors.length) % dashes.length
|
|
231
|
-
]
|
|
232
|
-
}
|
|
233
|
-
/>
|
|
234
|
-
</svg>
|
|
235
|
-
<span className="min-w-0 truncate font-mono">
|
|
236
|
-
{series.releaseId}
|
|
237
|
-
</span>
|
|
238
|
-
{series.points.some((point) => point.spike) ? (
|
|
239
|
-
<TriangleAlert
|
|
240
|
-
aria-label="Rollback spike"
|
|
241
|
-
className="size-3 shrink-0"
|
|
242
|
-
/>
|
|
243
|
-
) : null}
|
|
244
|
-
<span className="ml-auto tabular-nums">
|
|
245
|
-
{(metric === "active"
|
|
246
|
-
? series.activeInstallations
|
|
247
|
-
: series.recoveredInstallations
|
|
248
|
-
).toLocaleString()}
|
|
249
|
-
</span>
|
|
250
|
-
</button>
|
|
251
|
-
))}
|
|
252
|
-
</div>
|
|
253
|
-
</ScrollArea>
|
|
254
|
-
{selected ? (
|
|
255
|
-
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
|
|
256
|
-
<div className="min-w-0 flex-1">
|
|
257
|
-
<p className="break-all font-mono text-xs">
|
|
258
|
-
{selected.releaseId}
|
|
259
|
-
</p>
|
|
260
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
261
|
-
Active {selected.activeInstallations.toLocaleString()} ·
|
|
262
|
-
Rollback {selected.recoveredInstallations.toLocaleString()}{" "}
|
|
263
|
-
in this period
|
|
264
|
-
</p>
|
|
265
|
-
{metric === "rollback" && lastSpike ? (
|
|
266
|
-
<p className="mt-2 flex items-center gap-2 text-xs">
|
|
267
|
-
<TriangleAlert
|
|
268
|
-
className="size-3 shrink-0"
|
|
269
|
-
aria-hidden="true"
|
|
270
|
-
/>
|
|
271
|
-
Rollback spike on {dates.format(lastSpike.startMs)}.
|
|
272
|
-
Review this bundle’s delivery settings.
|
|
273
|
-
</p>
|
|
274
|
-
) : null}
|
|
275
|
-
</div>
|
|
276
|
-
<Link
|
|
277
|
-
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
278
|
-
to="/"
|
|
279
|
-
search={{ releaseId: selected.releaseId }}
|
|
280
|
-
>
|
|
281
|
-
Review bundle
|
|
282
|
-
<ArrowUpRight aria-hidden="true" />
|
|
283
|
-
</Link>
|
|
284
|
-
</div>
|
|
285
|
-
) : null}
|
|
286
|
-
<details className="text-xs text-muted-foreground">
|
|
287
|
-
<summary className="cursor-pointer">View chart data</summary>
|
|
288
|
-
<div className="mt-2 max-h-64 overflow-auto">
|
|
289
|
-
<table className="w-full text-left">
|
|
290
|
-
<caption className="sr-only">
|
|
291
|
-
{metric === "active"
|
|
292
|
-
? "Active installations"
|
|
293
|
-
: "Recovered installations"}{" "}
|
|
294
|
-
by bundle ID, UTC
|
|
295
|
-
</caption>
|
|
296
|
-
<thead>
|
|
297
|
-
<tr>
|
|
298
|
-
<th scope="col" className="pr-4">
|
|
299
|
-
Date (UTC)
|
|
300
|
-
</th>
|
|
301
|
-
{report.series.map((series) => (
|
|
302
|
-
<th
|
|
303
|
-
scope="col"
|
|
304
|
-
className="px-2 font-mono"
|
|
305
|
-
key={series.releaseId}
|
|
306
|
-
>
|
|
307
|
-
{series.releaseId}
|
|
308
|
-
</th>
|
|
309
|
-
))}
|
|
310
|
-
</tr>
|
|
311
|
-
</thead>
|
|
312
|
-
<tbody>
|
|
313
|
-
{data.map((point, i) => (
|
|
314
|
-
<tr key={point.startMs}>
|
|
315
|
-
<th
|
|
316
|
-
scope="row"
|
|
317
|
-
className="whitespace-nowrap pr-4 font-normal"
|
|
318
|
-
>
|
|
319
|
-
{times.format(point.startMs)}
|
|
320
|
-
</th>
|
|
321
|
-
{report.series.map((series) => (
|
|
322
|
-
<td
|
|
323
|
-
className="px-2 tabular-nums"
|
|
324
|
-
key={series.releaseId}
|
|
325
|
-
>
|
|
326
|
-
{(metric === "active"
|
|
327
|
-
? series.points[i].active
|
|
328
|
-
: series.points[i].recoveredInstallations) ?? "—"}
|
|
329
|
-
</td>
|
|
330
|
-
))}
|
|
331
|
-
</tr>
|
|
332
|
-
))}
|
|
333
|
-
</tbody>
|
|
334
|
-
</table>
|
|
335
|
-
</div>
|
|
336
|
-
</details>
|
|
337
|
-
</>
|
|
338
|
-
)}
|
|
339
|
-
</TabsContent>
|
|
340
|
-
</Tabs>
|
|
102
|
+
<line
|
|
103
|
+
x1="0"
|
|
104
|
+
y1="6"
|
|
105
|
+
x2="24"
|
|
106
|
+
y2="6"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
strokeWidth="2"
|
|
109
|
+
strokeDasharray={dashed ? "6 4" : undefined}
|
|
110
|
+
/>
|
|
111
|
+
</svg>
|
|
341
112
|
);
|
|
342
113
|
}
|
|
343
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Release health: how the newest bundles fare after their deployment, on
|
|
117
|
+
* one timeline, per interval. Adoption shows installations downloading each
|
|
118
|
+
* bundle (dashed) and launching it (solid), so the gap is the restarts still
|
|
119
|
+
* to come; Crashes shows them crashing back from it, and offers a rollback
|
|
120
|
+
* when a bundle crashes for too many of them.
|
|
121
|
+
*/
|
|
344
122
|
export function InsightsOverview({
|
|
345
123
|
input,
|
|
124
|
+
chart,
|
|
125
|
+
onChartChange,
|
|
126
|
+
health,
|
|
127
|
+
onReleasesChange,
|
|
346
128
|
onWindowChange,
|
|
347
|
-
query,
|
|
348
|
-
onRefresh,
|
|
349
129
|
}: {
|
|
350
130
|
readonly input: RecoveryInput;
|
|
351
|
-
readonly
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
};
|
|
357
|
-
readonly onRefresh: () => void;
|
|
131
|
+
readonly chart: HealthChart;
|
|
132
|
+
readonly onChartChange: (chart: HealthChart) => void;
|
|
133
|
+
readonly health: ReleaseHealthState;
|
|
134
|
+
/** Chooses the bundles to compare; undefined returns to the default. */
|
|
135
|
+
readonly onReleasesChange: (releaseIds?: readonly string[]) => void;
|
|
358
136
|
readonly onWindowChange: (window: RecoveryInput["window"]) => void;
|
|
359
137
|
}) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
138
|
+
const [active, setActive] = useState<string | null>(null);
|
|
139
|
+
const { period, candidates, releases } = health;
|
|
140
|
+
const ids = (releases ?? []).map(({ release }) => release.releaseId);
|
|
141
|
+
const colorOf = useSeriesColors(ids);
|
|
142
|
+
const addable = (candidates ?? []).filter(
|
|
143
|
+
(candidate) => !ids.includes(candidate.releaseId),
|
|
144
|
+
);
|
|
145
|
+
const full = ids.length >= MAX_ADOPTION_RELEASES;
|
|
146
|
+
const wider =
|
|
147
|
+
period.durationMs < 7 * DAY
|
|
148
|
+
? "7d"
|
|
149
|
+
: period.durationMs < 30 * DAY
|
|
150
|
+
? "30d"
|
|
151
|
+
: null;
|
|
152
|
+
const reset = health.isDefault ? null : (
|
|
153
|
+
<Button
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="sm"
|
|
156
|
+
onClick={() => onReleasesChange(undefined)}
|
|
365
157
|
>
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
158
|
+
<RotateCcw aria-hidden="true" data-icon="inline-start" />
|
|
159
|
+
Show the newest {DEFAULT_ADOPTION_RELEASES}
|
|
160
|
+
</Button>
|
|
161
|
+
);
|
|
162
|
+
// Adoption draws each bundle's launches solid and its downloads dashed;
|
|
163
|
+
// Crashes draws its crashes.
|
|
164
|
+
const seriesOf = (bundle: ComparedBundle) =>
|
|
165
|
+
chart === "adoption" ? bundle.launched : bundle.recovered;
|
|
166
|
+
const loaded = (releases ?? []).filter(
|
|
167
|
+
(bundle) =>
|
|
168
|
+
seriesOf(bundle) !== undefined &&
|
|
169
|
+
(chart !== "adoption" || bundle.downloaded !== undefined),
|
|
170
|
+
);
|
|
171
|
+
const failed =
|
|
172
|
+
(releases ?? []).find(({ error }) => error !== null)?.error ?? null;
|
|
173
|
+
const flagged = (releases ?? []).flatMap((bundle) => {
|
|
174
|
+
const crashes = crashesOf(bundle);
|
|
175
|
+
return crashes?.recommended ? [{ ...bundle, ...crashes }] : [];
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const remove = (releaseId: string) => {
|
|
179
|
+
const rest = ids.filter((id) => id !== releaseId);
|
|
180
|
+
onReleasesChange(rest.length === 0 ? undefined : rest);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const heading =
|
|
184
|
+
chart === "adoption" ? (
|
|
185
|
+
<div className="flex flex-col gap-1">
|
|
186
|
+
<div className="flex items-center gap-1 text-sm font-medium">
|
|
187
|
+
Installations downloading and launching each bundle
|
|
188
|
+
<InsightsInfo label="About adoption">
|
|
189
|
+
Dashed: installations that downloaded the bundle. Solid:
|
|
190
|
+
installations that launched it. After a forced update the two lines
|
|
191
|
+
meet.
|
|
192
|
+
</InsightsInfo>
|
|
383
193
|
</div>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
194
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
195
|
+
<p>
|
|
196
|
+
{cadenceOf(period.intervalMs)} · last {lengthOf(period.durationMs)}{" "}
|
|
197
|
+
· UTC
|
|
198
|
+
</p>
|
|
199
|
+
<ul aria-label="Line styles" className="flex items-center gap-3">
|
|
200
|
+
<li className="flex items-center gap-1.5">
|
|
201
|
+
<LineSample dashed />
|
|
202
|
+
Downloaded
|
|
203
|
+
</li>
|
|
204
|
+
<li className="flex items-center gap-1.5">
|
|
205
|
+
<LineSample />
|
|
206
|
+
Launched
|
|
207
|
+
</li>
|
|
208
|
+
</ul>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<div className="flex flex-col gap-1">
|
|
213
|
+
<div className="flex items-center gap-1 text-sm font-medium">
|
|
214
|
+
Crashes after each deployment
|
|
215
|
+
<InsightsInfo label="About crashes">
|
|
216
|
+
Installations that crashed on a bundle and rolled back. Crash rate =
|
|
217
|
+
crashed ÷ (launched + crashed). A rollback is suggested at{" "}
|
|
218
|
+
{percent(CRASH_RATE_THRESHOLD)} once {CRASH_MIN_ATTEMPTS}{" "}
|
|
219
|
+
installations tried it.
|
|
220
|
+
</InsightsInfo>
|
|
221
|
+
</div>
|
|
222
|
+
<p className="text-xs text-muted-foreground">
|
|
223
|
+
{cadenceOf(period.intervalMs)} · last {lengthOf(period.durationMs)} ·
|
|
224
|
+
UTC
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const body = (() => {
|
|
230
|
+
if (releases === undefined) {
|
|
231
|
+
return health.error ? (
|
|
232
|
+
<InsightsErrorAlert
|
|
233
|
+
error={health.error}
|
|
234
|
+
fallbackTitle="Release health unavailable"
|
|
235
|
+
/>
|
|
236
|
+
) : (
|
|
237
|
+
<Skeleton aria-label="Loading release health" className="h-64" />
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (releases.length === 0) {
|
|
241
|
+
return (
|
|
242
|
+
<Empty className="min-h-64">
|
|
243
|
+
<EmptyHeader>
|
|
244
|
+
<EmptyTitle>No bundles deployed here yet</EmptyTitle>
|
|
245
|
+
<EmptyDescription>
|
|
246
|
+
Release health follows the bundle deployments of the selected
|
|
247
|
+
channel and health platform. Deploy one, or choose another in the
|
|
248
|
+
filters.
|
|
249
|
+
</EmptyDescription>
|
|
250
|
+
</EmptyHeader>
|
|
251
|
+
{reset ? <EmptyContent>{reset}</EmptyContent> : null}
|
|
252
|
+
</Empty>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (loaded.length === 0) {
|
|
256
|
+
return failed ? (
|
|
257
|
+
<InsightsErrorAlert
|
|
258
|
+
error={failed}
|
|
259
|
+
fallbackTitle="Release health unavailable"
|
|
260
|
+
/>
|
|
261
|
+
) : (
|
|
262
|
+
<Skeleton aria-label="Loading release health" className="h-64" />
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
loaded.every(
|
|
267
|
+
(bundle) =>
|
|
268
|
+
seriesOf(bundle)!.total === 0 &&
|
|
269
|
+
(chart !== "adoption" || bundle.downloaded!.total === 0),
|
|
270
|
+
)
|
|
271
|
+
) {
|
|
272
|
+
return chart === "adoption" ? (
|
|
273
|
+
<Empty className="min-h-48">
|
|
274
|
+
<EmptyHeader>
|
|
275
|
+
<EmptyTitle>
|
|
276
|
+
No installation downloaded or launched these bundles in the last{" "}
|
|
277
|
+
{lengthOf(period.durationMs)}
|
|
278
|
+
</EmptyTitle>
|
|
279
|
+
<EmptyDescription>
|
|
280
|
+
{wider
|
|
281
|
+
? "Choose a longer period, or refresh after apps download them."
|
|
282
|
+
: "Refresh after apps download them."}
|
|
283
|
+
</EmptyDescription>
|
|
284
|
+
</EmptyHeader>
|
|
285
|
+
{wider ? (
|
|
286
|
+
<EmptyContent>
|
|
287
|
+
<Button
|
|
288
|
+
variant="outline"
|
|
289
|
+
size="sm"
|
|
290
|
+
onClick={() => onWindowChange(wider)}
|
|
291
|
+
>
|
|
292
|
+
Show {lengthOf(wider === "7d" ? 7 * DAY : 30 * DAY)}
|
|
293
|
+
</Button>
|
|
294
|
+
</EmptyContent>
|
|
295
|
+
) : null}
|
|
296
|
+
</Empty>
|
|
297
|
+
) : (
|
|
298
|
+
<Empty className="min-h-48">
|
|
299
|
+
<EmptyHeader>
|
|
300
|
+
<EmptyTitle>
|
|
301
|
+
No crashes in the last {lengthOf(period.durationMs)}
|
|
302
|
+
</EmptyTitle>
|
|
303
|
+
<EmptyDescription>
|
|
304
|
+
No launch of these bundles crashed and recovered.
|
|
305
|
+
</EmptyDescription>
|
|
306
|
+
</EmptyHeader>
|
|
307
|
+
</Empty>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return (
|
|
311
|
+
<ComparedBundlesChart
|
|
312
|
+
period={period}
|
|
313
|
+
lines={releases.map((bundle) => ({
|
|
314
|
+
release: bundle.release,
|
|
315
|
+
points: seriesOf(bundle)?.points,
|
|
316
|
+
...(chart === "adoption"
|
|
317
|
+
? { dashed: bundle.downloaded?.points }
|
|
318
|
+
: {}),
|
|
319
|
+
measuredAtMs: seriesOf(bundle)?.measuredAtMs,
|
|
320
|
+
}))}
|
|
321
|
+
colorOf={colorOf}
|
|
322
|
+
active={active}
|
|
323
|
+
names={
|
|
324
|
+
chart === "adoption"
|
|
325
|
+
? { solid: "Launched", dashed: "Downloaded" }
|
|
326
|
+
: { solid: "Crashed" }
|
|
327
|
+
}
|
|
328
|
+
label={
|
|
329
|
+
chart === "adoption"
|
|
330
|
+
? "Installations downloading (dashed) and launching (solid) each bundle per interval"
|
|
331
|
+
: "Crashes of each bundle per interval"
|
|
332
|
+
}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<section aria-label="Release health">
|
|
339
|
+
<Card className="min-w-0 overflow-hidden shadow-sm">
|
|
340
|
+
<CardHeader className="flex flex-col gap-4 p-6">
|
|
341
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
342
|
+
<CardTitle>Release health</CardTitle>
|
|
343
|
+
<div className="flex items-center gap-2">
|
|
344
|
+
<InsightsPeriodSelector
|
|
345
|
+
window={input.window}
|
|
346
|
+
onWindowChange={onWindowChange}
|
|
347
|
+
/>
|
|
348
|
+
<Button
|
|
349
|
+
aria-label="Refresh release health"
|
|
350
|
+
variant="ghost"
|
|
351
|
+
size="icon-lg"
|
|
352
|
+
disabled={health.isFetching}
|
|
353
|
+
onClick={health.refresh}
|
|
354
|
+
>
|
|
355
|
+
<RotateCw
|
|
356
|
+
aria-hidden="true"
|
|
357
|
+
className={cn(
|
|
358
|
+
health.isFetching && "motion-safe:animate-spin",
|
|
359
|
+
)}
|
|
360
|
+
/>
|
|
361
|
+
</Button>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</CardHeader>
|
|
365
|
+
<CardContent
|
|
366
|
+
aria-busy={health.isFetching}
|
|
367
|
+
className="flex flex-col gap-5 px-6 pb-6"
|
|
368
|
+
>
|
|
369
|
+
<Tabs
|
|
370
|
+
value={chart}
|
|
371
|
+
onValueChange={(value) => onChartChange(value as HealthChart)}
|
|
372
|
+
className="gap-5"
|
|
373
|
+
>
|
|
374
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
375
|
+
<TabsList
|
|
376
|
+
aria-label="Release health chart"
|
|
377
|
+
className="min-h-11 sm:min-h-9"
|
|
378
|
+
>
|
|
379
|
+
<TabsTrigger value="adoption" className="px-3">
|
|
380
|
+
Adoption
|
|
381
|
+
</TabsTrigger>
|
|
382
|
+
<TabsTrigger value="crashes" className="px-3">
|
|
383
|
+
Crashes
|
|
384
|
+
{flagged.length > 0 ? (
|
|
385
|
+
<>
|
|
386
|
+
<TriangleAlert
|
|
387
|
+
aria-hidden="true"
|
|
388
|
+
className="text-destructive"
|
|
389
|
+
/>
|
|
390
|
+
<span className="sr-only">, rollback recommended</span>
|
|
391
|
+
</>
|
|
392
|
+
) : null}
|
|
393
|
+
</TabsTrigger>
|
|
394
|
+
</TabsList>
|
|
395
|
+
{reset}
|
|
396
|
+
</div>
|
|
397
|
+
<TabsContent value="adoption" className="flex flex-col gap-4">
|
|
398
|
+
{heading}
|
|
399
|
+
{chart === "adoption" ? body : null}
|
|
400
|
+
</TabsContent>
|
|
401
|
+
<TabsContent value="crashes" className="flex flex-col gap-4">
|
|
402
|
+
{flagged.map((bundle) => (
|
|
403
|
+
<Alert key={bundle.release.releaseId} variant="destructive">
|
|
404
|
+
<TriangleAlert aria-hidden="true" />
|
|
405
|
+
<AlertTitle>
|
|
406
|
+
Roll back{" "}
|
|
407
|
+
<BundleIdDisplay
|
|
408
|
+
bundleId={bundle.release.releaseId}
|
|
409
|
+
className="text-sm"
|
|
410
|
+
/>
|
|
411
|
+
</AlertTitle>
|
|
412
|
+
<AlertDescription className="flex flex-col items-start gap-3">
|
|
413
|
+
It crashed for {percent(bundle.rate)} of the installations
|
|
414
|
+
that tried it ({bundle.crashes.toLocaleString()} of{" "}
|
|
415
|
+
{bundle.attempts.toLocaleString()}), above{" "}
|
|
416
|
+
{percent(CRASH_RATE_THRESHOLD)}.
|
|
417
|
+
<RollbackButton
|
|
418
|
+
release={bundle.release}
|
|
419
|
+
rate={bundle.rate}
|
|
420
|
+
crashes={bundle.crashes}
|
|
421
|
+
attempts={bundle.attempts}
|
|
422
|
+
/>
|
|
423
|
+
</AlertDescription>
|
|
424
|
+
</Alert>
|
|
425
|
+
))}
|
|
426
|
+
{heading}
|
|
427
|
+
{chart === "crashes" ? body : null}
|
|
428
|
+
</TabsContent>
|
|
429
|
+
</Tabs>
|
|
430
|
+
{releases !== undefined && releases.length > 0 ? (
|
|
403
431
|
<>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
<p>
|
|
410
|
-
{query.data.unattributedInstallations.toLocaleString()}{" "}
|
|
411
|
-
reporting installations have no observed bundle ID and are
|
|
412
|
-
excluded.
|
|
413
|
-
</p>
|
|
432
|
+
{failed && loaded.length > 0 ? (
|
|
433
|
+
<InsightsErrorAlert
|
|
434
|
+
error={failed}
|
|
435
|
+
fallbackTitle="Some bundles could not be read"
|
|
436
|
+
/>
|
|
414
437
|
) : null}
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
438
|
+
<Table aria-label="Compared bundles">
|
|
439
|
+
<TableHeader>
|
|
440
|
+
<TableRow>
|
|
441
|
+
<TableHead>Bundle</TableHead>
|
|
442
|
+
{chart === "adoption" ? (
|
|
443
|
+
<>
|
|
444
|
+
<TableHead className="text-right">Downloaded</TableHead>
|
|
445
|
+
<TableHead className="text-right">Launched</TableHead>
|
|
446
|
+
</>
|
|
447
|
+
) : (
|
|
448
|
+
<>
|
|
449
|
+
<TableHead className="text-right">Crashed</TableHead>
|
|
450
|
+
<TableHead className="text-right">Crash rate</TableHead>
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
<TableHead className="w-10">
|
|
454
|
+
<span className="sr-only">Remove</span>
|
|
455
|
+
</TableHead>
|
|
456
|
+
</TableRow>
|
|
457
|
+
</TableHeader>
|
|
458
|
+
<TableBody>
|
|
459
|
+
{releases.map((bundle) => {
|
|
460
|
+
const { release } = bundle;
|
|
461
|
+
const crashes = crashesOf(bundle);
|
|
462
|
+
return (
|
|
463
|
+
<TableRow
|
|
464
|
+
key={release.releaseId}
|
|
465
|
+
data-state={
|
|
466
|
+
active === release.releaseId ? "selected" : undefined
|
|
467
|
+
}
|
|
468
|
+
onMouseEnter={() => setActive(release.releaseId)}
|
|
469
|
+
onMouseLeave={() => setActive(null)}
|
|
470
|
+
onFocus={() => setActive(release.releaseId)}
|
|
471
|
+
onBlur={() => setActive(null)}
|
|
472
|
+
>
|
|
473
|
+
{/* The bundle cell takes the room left, and wraps in it. */}
|
|
474
|
+
<TableCell className="w-full max-w-0 whitespace-normal">
|
|
475
|
+
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
|
|
476
|
+
{/* Narrower on a phone, so a bundle's ID keeps more of the row. */}
|
|
477
|
+
<svg
|
|
478
|
+
aria-hidden="true"
|
|
479
|
+
className="h-3 w-4 shrink-0 sm:w-6"
|
|
480
|
+
viewBox="0 0 24 12"
|
|
481
|
+
preserveAspectRatio="none"
|
|
482
|
+
>
|
|
483
|
+
<line
|
|
484
|
+
x1="0"
|
|
485
|
+
y1="6"
|
|
486
|
+
x2="24"
|
|
487
|
+
y2="6"
|
|
488
|
+
stroke={colorOf(release.releaseId)}
|
|
489
|
+
strokeWidth="3"
|
|
490
|
+
/>
|
|
491
|
+
</svg>
|
|
492
|
+
<div className="flex min-w-0 flex-col">
|
|
493
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
494
|
+
<BundleIdDisplay
|
|
495
|
+
bundleId={release.releaseId}
|
|
496
|
+
className="min-w-0"
|
|
497
|
+
/>
|
|
498
|
+
{release.enabled ? null : (
|
|
499
|
+
<Badge variant="outline">Disabled</Badge>
|
|
500
|
+
)}
|
|
501
|
+
</span>
|
|
502
|
+
<span className="text-xs break-words text-muted-foreground">
|
|
503
|
+
Deployed{" "}
|
|
504
|
+
{formatDeployedAt(release.deployedAtMs)}
|
|
505
|
+
{release.targetAppVersion
|
|
506
|
+
? ` · ${release.targetAppVersion}`
|
|
507
|
+
: ""}
|
|
508
|
+
</span>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</TableCell>
|
|
512
|
+
{chart === "adoption" ? (
|
|
513
|
+
<>
|
|
514
|
+
<TableCell className="text-right whitespace-nowrap tabular-nums">
|
|
515
|
+
{bundle.downloaded?.total.toLocaleString() ?? "—"}
|
|
516
|
+
</TableCell>
|
|
517
|
+
<TableCell className="text-right font-medium whitespace-nowrap tabular-nums">
|
|
518
|
+
{bundle.launched?.total.toLocaleString() ?? "—"}
|
|
519
|
+
</TableCell>
|
|
520
|
+
</>
|
|
521
|
+
) : (
|
|
522
|
+
<>
|
|
523
|
+
<TableCell className="text-right whitespace-nowrap tabular-nums">
|
|
524
|
+
{crashes === undefined ? (
|
|
525
|
+
"—"
|
|
526
|
+
) : crashes.crashes === 0 ? (
|
|
527
|
+
<span className="text-muted-foreground">0</span>
|
|
528
|
+
) : (
|
|
529
|
+
crashes.crashes.toLocaleString()
|
|
530
|
+
)}
|
|
531
|
+
</TableCell>
|
|
532
|
+
<TableCell className="text-right whitespace-nowrap tabular-nums">
|
|
533
|
+
{crashes === undefined ||
|
|
534
|
+
crashes.attempts === 0 ? (
|
|
535
|
+
"—"
|
|
536
|
+
) : crashes.recommended ? (
|
|
537
|
+
<span className="inline-flex items-center gap-1 font-medium text-destructive">
|
|
538
|
+
<TriangleAlert
|
|
539
|
+
aria-hidden="true"
|
|
540
|
+
className="size-3.5"
|
|
541
|
+
/>
|
|
542
|
+
{percent(crashes.rate)}
|
|
543
|
+
<span className="sr-only">
|
|
544
|
+
, rollback recommended
|
|
545
|
+
</span>
|
|
546
|
+
</span>
|
|
547
|
+
) : (
|
|
548
|
+
<span
|
|
549
|
+
className={cn(
|
|
550
|
+
crashes.attempts < CRASH_MIN_ATTEMPTS &&
|
|
551
|
+
"text-muted-foreground",
|
|
552
|
+
)}
|
|
553
|
+
title={
|
|
554
|
+
crashes.attempts < CRASH_MIN_ATTEMPTS
|
|
555
|
+
? `Only ${crashes.attempts} installations tried it`
|
|
556
|
+
: undefined
|
|
557
|
+
}
|
|
558
|
+
>
|
|
559
|
+
{percent(crashes.rate)}
|
|
560
|
+
</span>
|
|
561
|
+
)}
|
|
562
|
+
</TableCell>
|
|
563
|
+
</>
|
|
564
|
+
)}
|
|
565
|
+
<TableCell>
|
|
566
|
+
<Button
|
|
567
|
+
aria-label={`Remove ${release.releaseId}`}
|
|
568
|
+
variant="ghost"
|
|
569
|
+
size="icon-xs"
|
|
570
|
+
onClick={() => remove(release.releaseId)}
|
|
571
|
+
>
|
|
572
|
+
<X aria-hidden="true" />
|
|
573
|
+
</Button>
|
|
574
|
+
</TableCell>
|
|
575
|
+
</TableRow>
|
|
576
|
+
);
|
|
577
|
+
})}
|
|
578
|
+
</TableBody>
|
|
579
|
+
</Table>
|
|
580
|
+
{addable.length > 0 ? (
|
|
581
|
+
<Select
|
|
582
|
+
items={Object.fromEntries(
|
|
583
|
+
addable.map((candidate) => [
|
|
584
|
+
candidate.releaseId,
|
|
585
|
+
`${candidate.releaseId} · ${formatDeployedAt(candidate.deployedAtMs)}`,
|
|
586
|
+
]),
|
|
587
|
+
)}
|
|
588
|
+
value={null}
|
|
589
|
+
disabled={full}
|
|
590
|
+
onValueChange={(value) => {
|
|
591
|
+
if (value !== null) onReleasesChange([...ids, value]);
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
<SelectTrigger
|
|
595
|
+
aria-label="Add a bundle"
|
|
596
|
+
className="min-h-11 w-full sm:min-h-9 sm:w-80"
|
|
597
|
+
>
|
|
598
|
+
<SelectValue
|
|
599
|
+
placeholder={
|
|
600
|
+
full
|
|
601
|
+
? `Up to ${MAX_ADOPTION_RELEASES} bundles`
|
|
602
|
+
: "Add a bundle"
|
|
603
|
+
}
|
|
604
|
+
/>
|
|
605
|
+
</SelectTrigger>
|
|
606
|
+
<SelectContent>
|
|
607
|
+
<SelectGroup>
|
|
608
|
+
{addable.map((candidate) => (
|
|
609
|
+
<SelectItem
|
|
610
|
+
key={candidate.releaseId}
|
|
611
|
+
value={candidate.releaseId}
|
|
612
|
+
>
|
|
613
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
614
|
+
<BundleIdDisplay bundleId={candidate.releaseId} />{" "}
|
|
615
|
+
<span className="text-xs text-muted-foreground">
|
|
616
|
+
Deployed{" "}
|
|
617
|
+
{formatDeployedAt(candidate.deployedAtMs)}
|
|
618
|
+
</span>
|
|
619
|
+
</span>
|
|
620
|
+
</SelectItem>
|
|
621
|
+
))}
|
|
622
|
+
</SelectGroup>
|
|
623
|
+
</SelectContent>
|
|
624
|
+
</Select>
|
|
421
625
|
) : null}
|
|
422
626
|
</>
|
|
423
|
-
) :
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
>
|
|
437
|
-
<ListIcon aria-hidden="true" data-icon="inline-start" />
|
|
438
|
-
All events
|
|
439
|
-
</Link>
|
|
440
|
-
</CardFooter>
|
|
441
|
-
</Card>
|
|
627
|
+
) : null}
|
|
628
|
+
</CardContent>
|
|
629
|
+
<CardFooter className="flex items-center justify-end border-t px-6 py-5">
|
|
630
|
+
<Link
|
|
631
|
+
className={buttonVariants({ variant: "outline", size: "sm" })}
|
|
632
|
+
to="/installations"
|
|
633
|
+
>
|
|
634
|
+
<ListIcon aria-hidden="true" />
|
|
635
|
+
Event history
|
|
636
|
+
</Link>
|
|
637
|
+
</CardFooter>
|
|
638
|
+
</Card>
|
|
639
|
+
</section>
|
|
442
640
|
);
|
|
443
641
|
}
|