@hot-updater/console 0.36.7 → 1.0.0-rc.1
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/BundleIdDisplay-D4ErdU5_.js +1 -0
- package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +1 -0
- package/.output/public/assets/api-keys-api-DGVTTvTJ.js +1 -0
- package/.output/public/assets/api-keys-z16OZIP4.js +1 -0
- package/.output/public/assets/badge-Br_KDO0J.js +1 -0
- package/.output/public/assets/card-DzuEXvyJ.js +1 -0
- package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
- package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
- package/.output/public/assets/dialog-BeAENgez.js +1 -0
- package/.output/public/assets/dist-Bssh_cv0.js +1 -0
- package/.output/public/assets/field-Buq148-A.js +1 -0
- package/.output/public/assets/index-uOpQAIC4.js +10 -0
- package/.output/public/assets/input-group-B3p5UpyW.js +1 -0
- package/.output/public/assets/insights-DJTr7Ozn.js +1 -0
- package/.output/public/assets/insights-api-DVMOoRMS.js +1 -0
- package/.output/public/assets/installations-BY64iIAK.js +2 -0
- package/.output/public/assets/installations-D5v9i3AL.js +1 -0
- package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +1 -0
- package/.output/public/assets/preload-helper-CQ36nxok.js +1 -0
- package/.output/public/assets/refresh-cw-BkbfZ62O.js +1 -0
- package/.output/public/assets/routes-BuoI5-Cr.js +2 -0
- package/.output/public/assets/routes-C8fvgETO.js +1 -0
- package/.output/public/assets/search-qsIDvS2I.js +1 -0
- package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
- package/.output/public/assets/signing-mJD88Y4C.js +1 -0
- package/.output/public/assets/styles-BZIG7t42.css +2 -0
- package/.output/public/assets/table-RamFEclP.js +1 -0
- package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +42 -0
- package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
- package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs +146 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
- 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 +1134 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +141 -13
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14117 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__query-core.mjs +114 -1
- package/.output/server/_libs/tanstack__react-query.mjs +3 -4
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +288 -0
- package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +13 -0
- package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +15 -0
- package/.output/server/_ssr/api-keys-CpBqHpuC.mjs +300 -0
- package/.output/server/_ssr/api-keys-api-Czkp57UV.mjs +60 -0
- package/.output/server/_ssr/api-keys-rpc-BTtMDSoR.mjs +136 -0
- package/.output/server/_ssr/api-rpc-BEX8ISai.mjs +1468 -0
- package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
- package/.output/server/_ssr/auth-rpc-BFMEC-TT.mjs +24 -0
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +47 -0
- package/.output/server/_ssr/badge-CpWXQwyF.mjs +26 -0
- package/.output/server/_ssr/bundle-signing-rpc-BbCFUQSu.mjs +16 -0
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
- package/.output/server/_ssr/card-Bsc8mRis.mjs +86 -0
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
- package/.output/server/_ssr/config.server-DQYEr_5X.mjs +361 -0
- package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
- package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
- package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-DhfrxEjg.mjs} +68 -50
- package/.output/server/_ssr/databaseRows-QlCYLi0H.mjs +166 -0
- package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
- package/.output/server/_ssr/dialog-C0vKUp8R.mjs +170 -0
- package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
- package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
- package/.output/server/_ssr/field-DASCh-5d.mjs +74 -0
- package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
- package/.output/server/_ssr/input-group-etIsBUqv.mjs +288 -0
- package/.output/server/_ssr/insights-api-C1F7UreR.mjs +71 -0
- package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +96 -0
- package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +155 -0
- package/.output/server/_ssr/insights-ygT_aIfo.mjs +858 -0
- package/.output/server/_ssr/installations-DqRT1QTd.mjs +535 -0
- package/.output/server/_ssr/installations-K8TCYTYh.mjs +19 -0
- package/.output/server/_ssr/router-BxBEvtF1.mjs +513 -0
- package/.output/server/_ssr/routes-BC7rn-tf.mjs +37 -0
- package/.output/server/_ssr/routes-CM5KAwpB.mjs +2285 -0
- package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +527 -0
- package/.output/server/_ssr/server-CpzW--lT.mjs +7468 -0
- package/.output/server/_ssr/signing-jk4HANnl.mjs +134 -0
- package/.output/server/_ssr/ssr.mjs +4 -5740
- package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
- package/.output/server/_ssr/table-CU58HeI7.mjs +53 -0
- package/.output/server/_ssr/uuidv7-CaKuKXrV.mjs +31 -0
- package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +103 -0
- package/.output/server/index.mjs +229 -47
- package/README.md +29 -8
- package/lib/index.d.mts +35 -0
- package/lib/index.mjs +4 -0
- package/lib/vite-internal.mjs +127 -0
- package/lib/vite.d.mts +10 -0
- package/lib/vite.mjs +5 -0
- package/package.json +51 -29
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +1 -0
- package/public/manifest.json +30 -0
- package/public/robots.txt +3 -0
- package/src/components/AppSidebar.tsx +159 -0
- package/src/components/BundleIdDisplay.tsx +18 -0
- package/src/components/ChannelBadge.tsx +31 -0
- package/src/components/ConsoleAccessPage.tsx +122 -0
- package/src/components/EnabledStatusIcon.tsx +54 -0
- package/src/components/HashValueDisplay.tsx +85 -0
- package/src/components/HotUpdaterLogo.tsx +30 -0
- package/src/components/NotFoundPage.tsx +33 -0
- package/src/components/PlatformIcon.tsx +39 -0
- package/src/components/RolloutPercentageBadge.tsx +28 -0
- package/src/components/ThemeProvider.tsx +116 -0
- package/src/components/TimestampDisplay.tsx +41 -0
- package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
- package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
- package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
- package/src/components/features/bundles/BundleInsightsSummary.tsx +123 -0
- package/src/components/features/bundles/BundleMetadata.tsx +136 -0
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
- package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
- package/src/components/features/insights/ActivityChart.tsx +150 -0
- package/src/components/features/insights/BundleDistribution.tsx +133 -0
- package/src/components/features/insights/BundleSelector.tsx +114 -0
- package/src/components/features/insights/InsightsCapabilityContext.tsx +24 -0
- package/src/components/features/insights/InsightsControls.tsx +115 -0
- package/src/components/features/insights/InsightsErrorAlert.tsx +21 -0
- package/src/components/features/insights/InsightsOverview.tsx +208 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +248 -0
- package/src/components/features/insights/InstallationMatchesCard.tsx +132 -0
- package/src/components/features/insights/InstallationPageHeader.tsx +138 -0
- package/src/components/features/insights/InstallationPagination.tsx +48 -0
- package/src/components/features/insights/InstallationSearch.tsx +230 -0
- package/src/components/features/insights/UpdateOutcomes.tsx +147 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
- package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
- package/src/components/features/signing/BundleSigningPage.tsx +139 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +89 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/combobox.tsx +303 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/dropdown-menu.tsx +270 -0
- package/src/components/ui/field.tsx +235 -0
- package/src/components/ui/input-group.tsx +147 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/progress.tsx +84 -0
- package/src/components/ui/select.tsx +205 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +132 -0
- package/src/components/ui/sidebar.tsx +779 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +56 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +30 -0
- package/src/index.ts +47 -0
- package/src/lib/api-keys-api.ts +69 -0
- package/src/lib/api-keys-rpc.ts +87 -0
- package/src/lib/api-rpc.ts +478 -0
- package/src/lib/api.ts +444 -0
- package/src/lib/auth-rpc.ts +16 -0
- package/src/lib/bundle-signing-api.ts +14 -0
- package/src/lib/bundle-signing-rpc.ts +16 -0
- package/src/lib/constants.ts +1 -0
- package/src/lib/git.ts +51 -0
- package/src/lib/insights-api.ts +144 -0
- package/src/lib/insights-error.ts +19 -0
- package/src/lib/insights-input.ts +187 -0
- package/src/lib/insights-overview.ts +170 -0
- package/src/lib/insights-rpc.ts +218 -0
- package/src/lib/server/auth.server.ts +43 -0
- package/src/lib/server/bundle-signing.server.ts +23 -0
- package/src/lib/server/config.server.ts +109 -0
- package/src/lib/server/console-runtime.server.ts +61 -0
- package/src/lib/server/deleteBundle.ts +145 -0
- package/src/lib/server/downloadBundle.ts +46 -0
- package/src/lib/server/getBundleChildren.ts +130 -0
- package/src/lib/server/listReleases.ts +61 -0
- package/src/lib/server/releaseActivity.ts +27 -0
- package/src/lib/server/releaseReachability.ts +95 -0
- package/src/lib/server/runtime.server.ts +172 -0
- package/src/lib/utils.ts +9 -0
- package/src/logo.svg +1 -0
- package/src/routeTree.gen.ts +199 -0
- package/src/router.tsx +21 -0
- package/src/routes/-installations-search.ts +19 -0
- package/src/routes/-releases-search.ts +74 -0
- package/src/routes/__root.tsx +174 -0
- package/src/routes/api/auth/$.ts +19 -0
- package/src/routes/api/bundles/$bundleId/download.ts +17 -0
- package/src/routes/api-keys.tsx +37 -0
- package/src/routes/index.tsx +982 -0
- package/src/routes/insights.tsx +139 -0
- package/src/routes/installations.tsx +194 -0
- package/src/routes/signing.tsx +37 -0
- package/src/styles.css +152 -0
- package/src/virtual-modules.d.ts +21 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-internal.ts +195 -0
- package/src/vite.ts +12 -0
- package/.output/public/assets/dist-DJX53faP.js +0 -3
- package/.output/public/assets/index-Iylis5kY.js +0 -10
- package/.output/public/assets/routes-HT21hs0C.js +0 -54
- package/.output/public/assets/styles-C0Re4quB.css +0 -2
- package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
- package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
- package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
- package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
- package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
- package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
- package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
- package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
- package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
- package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
- package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
- package/.output/server/_libs/radix-ui__number.mjs +0 -6
- package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
- package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
- package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
- package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
- package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
- package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
- package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
- package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
- package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
- package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
- package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
- package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
- package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
- package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
- package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
- package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
- package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/.output/server/package.json +0 -9
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ActiveInstallationWindow } from "@hot-updater/server";
|
|
2
|
+
import { Search, X } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
7
|
+
import {
|
|
8
|
+
InputGroup,
|
|
9
|
+
InputGroupAddon,
|
|
10
|
+
InputGroupButton,
|
|
11
|
+
InputGroupInput,
|
|
12
|
+
} from "@/components/ui/input-group";
|
|
13
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
14
|
+
|
|
15
|
+
const windows = [
|
|
16
|
+
{ value: "24h", label: "24 hours", shortLabel: "24h" },
|
|
17
|
+
{ value: "7d", label: "7 days", shortLabel: "7d" },
|
|
18
|
+
{ value: "30d", label: "30 days", shortLabel: "30d" },
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export function InsightsControls({
|
|
22
|
+
onInstallationSearch,
|
|
23
|
+
onWindowChange,
|
|
24
|
+
window,
|
|
25
|
+
}: {
|
|
26
|
+
readonly onInstallationSearch: (query: string) => void;
|
|
27
|
+
readonly onWindowChange: (window: ActiveInstallationWindow) => void;
|
|
28
|
+
readonly window: ActiveInstallationWindow;
|
|
29
|
+
}) {
|
|
30
|
+
const [draft, setDraft] = useState("");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section
|
|
34
|
+
aria-label="Insights controls"
|
|
35
|
+
className="rounded-xl border bg-background px-4 py-3"
|
|
36
|
+
>
|
|
37
|
+
<form
|
|
38
|
+
aria-label="Filter insights"
|
|
39
|
+
className="min-w-0"
|
|
40
|
+
onSubmit={(event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
const query = draft.trim();
|
|
43
|
+
if (query) onInstallationSearch(query);
|
|
44
|
+
}}
|
|
45
|
+
role="search"
|
|
46
|
+
>
|
|
47
|
+
<FieldGroup className="gap-3 md:grid md:grid-cols-[auto_minmax(18rem,1fr)] md:items-end">
|
|
48
|
+
<Field className="min-w-0">
|
|
49
|
+
<FieldLabel>Reporting period</FieldLabel>
|
|
50
|
+
<ToggleGroup
|
|
51
|
+
aria-label="Reporting period"
|
|
52
|
+
className="w-full lg:w-fit"
|
|
53
|
+
onValueChange={(value) => {
|
|
54
|
+
if (value[0]) {
|
|
55
|
+
onWindowChange(value[0] as ActiveInstallationWindow);
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
spacing={0}
|
|
59
|
+
size="lg"
|
|
60
|
+
value={[window]}
|
|
61
|
+
variant="outline"
|
|
62
|
+
>
|
|
63
|
+
{windows.map((item) => (
|
|
64
|
+
<ToggleGroupItem
|
|
65
|
+
aria-label={item.label}
|
|
66
|
+
className="flex-1 md:flex-none"
|
|
67
|
+
key={item.value}
|
|
68
|
+
value={item.value}
|
|
69
|
+
>
|
|
70
|
+
{item.shortLabel}
|
|
71
|
+
</ToggleGroupItem>
|
|
72
|
+
))}
|
|
73
|
+
</ToggleGroup>
|
|
74
|
+
</Field>
|
|
75
|
+
<Field className="min-w-0">
|
|
76
|
+
<FieldLabel htmlFor="installation-history-search">
|
|
77
|
+
Installation history
|
|
78
|
+
</FieldLabel>
|
|
79
|
+
<div className="flex min-w-0 flex-col gap-2 sm:flex-row">
|
|
80
|
+
<InputGroup className="h-8 flex-1">
|
|
81
|
+
<InputGroupAddon>
|
|
82
|
+
<Search aria-hidden="true" />
|
|
83
|
+
</InputGroupAddon>
|
|
84
|
+
<InputGroupInput
|
|
85
|
+
aria-label="User or install ID"
|
|
86
|
+
id="installation-history-search"
|
|
87
|
+
maxLength={1024}
|
|
88
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
89
|
+
placeholder="User ID or install ID"
|
|
90
|
+
type="search"
|
|
91
|
+
value={draft}
|
|
92
|
+
/>
|
|
93
|
+
{draft && (
|
|
94
|
+
<InputGroupAddon align="inline-end">
|
|
95
|
+
<InputGroupButton
|
|
96
|
+
aria-label="Clear installation search"
|
|
97
|
+
onClick={() => setDraft("")}
|
|
98
|
+
size="icon-xs"
|
|
99
|
+
>
|
|
100
|
+
<X aria-hidden="true" />
|
|
101
|
+
</InputGroupButton>
|
|
102
|
+
</InputGroupAddon>
|
|
103
|
+
)}
|
|
104
|
+
</InputGroup>
|
|
105
|
+
<Button className="w-full sm:w-auto" size="lg" type="submit">
|
|
106
|
+
<Search aria-hidden="true" data-icon="inline-start" />
|
|
107
|
+
Search
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</Field>
|
|
111
|
+
</FieldGroup>
|
|
112
|
+
</form>
|
|
113
|
+
</section>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TriangleAlert } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
4
|
+
import { getInsightsErrorCopy } from "@/lib/insights-error";
|
|
5
|
+
|
|
6
|
+
export function InsightsErrorAlert({
|
|
7
|
+
error,
|
|
8
|
+
fallbackTitle,
|
|
9
|
+
}: {
|
|
10
|
+
readonly error: Error;
|
|
11
|
+
readonly fallbackTitle: string;
|
|
12
|
+
}) {
|
|
13
|
+
const copy = getInsightsErrorCopy(error, fallbackTitle);
|
|
14
|
+
return (
|
|
15
|
+
<Alert variant="destructive">
|
|
16
|
+
<TriangleAlert aria-hidden="true" />
|
|
17
|
+
<AlertTitle>{copy.title}</AlertTitle>
|
|
18
|
+
<AlertDescription>{copy.description}</AlertDescription>
|
|
19
|
+
</Alert>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ActiveInstallationOverview } from "@hot-updater/server";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardFooter,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@/components/ui/card";
|
|
12
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
13
|
+
import type { InsightsOverview as CatalogOverview } from "@/lib/insights-overview";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
|
|
16
|
+
import { ActivityChart } from "./ActivityChart";
|
|
17
|
+
import { BundleSelector } from "./BundleSelector";
|
|
18
|
+
import { InsightsErrorAlert } from "./InsightsErrorAlert";
|
|
19
|
+
import { UpdateOutcomes, type UpdateOutcomeState } from "./UpdateOutcomes";
|
|
20
|
+
|
|
21
|
+
type InsightsOverviewProps =
|
|
22
|
+
| { readonly status: "loading" }
|
|
23
|
+
| { readonly status: "error"; readonly error: Error }
|
|
24
|
+
| {
|
|
25
|
+
readonly status: "success";
|
|
26
|
+
readonly active: ActiveInstallationOverview;
|
|
27
|
+
readonly bundleId: string;
|
|
28
|
+
readonly bundles: readonly {
|
|
29
|
+
readonly bundleId: string;
|
|
30
|
+
readonly description: string;
|
|
31
|
+
}[];
|
|
32
|
+
readonly catalog: CatalogOverview;
|
|
33
|
+
readonly onBundleChange: (bundleId: string) => void;
|
|
34
|
+
readonly outcomes: UpdateOutcomeState;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const asOfFormatter = new Intl.DateTimeFormat("en", {
|
|
38
|
+
dateStyle: "medium",
|
|
39
|
+
timeStyle: "short",
|
|
40
|
+
timeZone: "UTC",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const activityWindowCopy = {
|
|
44
|
+
"24h": {
|
|
45
|
+
label: "Daily active installations",
|
|
46
|
+
period: "last 24 hours",
|
|
47
|
+
},
|
|
48
|
+
"7d": {
|
|
49
|
+
label: "Weekly active installations",
|
|
50
|
+
period: "last 7 days",
|
|
51
|
+
},
|
|
52
|
+
"30d": {
|
|
53
|
+
label: "Monthly active installations",
|
|
54
|
+
period: "last 30 days",
|
|
55
|
+
},
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
function LoadingCard({
|
|
59
|
+
children,
|
|
60
|
+
className,
|
|
61
|
+
label,
|
|
62
|
+
}: {
|
|
63
|
+
readonly children: ReactNode;
|
|
64
|
+
readonly className?: string;
|
|
65
|
+
readonly label: string;
|
|
66
|
+
}) {
|
|
67
|
+
return (
|
|
68
|
+
<Card aria-label={label} className={cn("min-w-0", className)}>
|
|
69
|
+
<CardHeader>
|
|
70
|
+
<Skeleton className="h-4 w-32" />
|
|
71
|
+
<Skeleton className="h-3 w-4/5" />
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent>{children}</CardContent>
|
|
74
|
+
</Card>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function InsightsOverview(props: InsightsOverviewProps) {
|
|
79
|
+
if (props.status === "loading") {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
aria-label="Loading reporting insights"
|
|
83
|
+
className="flex min-w-0 flex-col gap-8"
|
|
84
|
+
>
|
|
85
|
+
<LoadingCard label="Loading installation activity">
|
|
86
|
+
<Skeleton className="h-10 w-24" />
|
|
87
|
+
<Skeleton className="mt-5 h-64 w-full" />
|
|
88
|
+
<div className="mt-5 grid gap-4 border-t pt-5 sm:grid-cols-3">
|
|
89
|
+
<Skeleton className="h-10 w-full" />
|
|
90
|
+
<Skeleton className="h-10 w-full" />
|
|
91
|
+
<Skeleton className="h-10 w-full" />
|
|
92
|
+
</div>
|
|
93
|
+
</LoadingCard>
|
|
94
|
+
<LoadingCard label="Loading bundle detail">
|
|
95
|
+
<div className="flex flex-col gap-4">
|
|
96
|
+
<Skeleton className="h-10 w-full max-w-md" />
|
|
97
|
+
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
98
|
+
<Skeleton className="h-14 w-full" />
|
|
99
|
+
<Skeleton className="h-14 w-full" />
|
|
100
|
+
<Skeleton className="h-14 w-full" />
|
|
101
|
+
<Skeleton className="h-14 w-full" />
|
|
102
|
+
</div>
|
|
103
|
+
<Skeleton className="h-32 w-full" />
|
|
104
|
+
</div>
|
|
105
|
+
</LoadingCard>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (props.status === "error") {
|
|
111
|
+
return (
|
|
112
|
+
<InsightsErrorAlert
|
|
113
|
+
error={props.error}
|
|
114
|
+
fallbackTitle="Reporting insights unavailable"
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { active, bundleId, bundles, catalog, onBundleChange, outcomes } =
|
|
120
|
+
props;
|
|
121
|
+
const activityCopy = activityWindowCopy[active.window];
|
|
122
|
+
const selectedBundleId =
|
|
123
|
+
outcomes.status === "idle" ? null : outcomes.bundleId;
|
|
124
|
+
const latestBundleInstallations =
|
|
125
|
+
active.bundles.find(({ bundleId }) => bundleId === selectedBundleId)
|
|
126
|
+
?.installations ?? 0;
|
|
127
|
+
const configuredPercentage =
|
|
128
|
+
catalog.configuredRollouts.find(
|
|
129
|
+
({ bundleId }) => bundleId === selectedBundleId,
|
|
130
|
+
)?.configuredPercentage ?? null;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex min-w-0 flex-col gap-8">
|
|
134
|
+
<section aria-label="Installation activity">
|
|
135
|
+
<Card className="min-w-0 overflow-hidden shadow-sm">
|
|
136
|
+
<CardHeader className="pb-4">
|
|
137
|
+
<CardTitle className="text-sm font-medium">
|
|
138
|
+
<h2>{activityCopy.label}</h2>
|
|
139
|
+
</CardTitle>
|
|
140
|
+
<CardDescription>
|
|
141
|
+
Unique app installations that reported an update status in the{" "}
|
|
142
|
+
{activityCopy.period}.
|
|
143
|
+
</CardDescription>
|
|
144
|
+
</CardHeader>
|
|
145
|
+
<CardContent className="flex flex-col gap-5">
|
|
146
|
+
<div className="flex flex-col gap-1">
|
|
147
|
+
<p className="text-xs font-medium text-muted-foreground">
|
|
148
|
+
Active installations
|
|
149
|
+
</p>
|
|
150
|
+
<div className="flex items-end gap-2">
|
|
151
|
+
<span className="text-4xl font-semibold tracking-tight tabular-nums">
|
|
152
|
+
{active.activeInstallations.toLocaleString()}
|
|
153
|
+
</span>
|
|
154
|
+
<span className="pb-1 text-xs text-muted-foreground">
|
|
155
|
+
unique in {activityCopy.period}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="border-t pt-5">
|
|
160
|
+
<ActivityChart series={active.series} window={active.window} />
|
|
161
|
+
</div>
|
|
162
|
+
</CardContent>
|
|
163
|
+
<CardFooter className="border-t bg-muted/15 p-0">
|
|
164
|
+
<dl className="grid w-full sm:grid-cols-3 sm:divide-x sm:divide-border/70">
|
|
165
|
+
<div className="flex flex-col gap-1 px-5 py-4">
|
|
166
|
+
<dt className="text-xs text-muted-foreground">
|
|
167
|
+
Reported bundles
|
|
168
|
+
</dt>
|
|
169
|
+
<dd className="text-lg font-semibold tabular-nums">
|
|
170
|
+
{active.bundles.length.toLocaleString()}
|
|
171
|
+
</dd>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex flex-col gap-1 border-t px-5 py-4 sm:border-t-0">
|
|
174
|
+
<dt className="text-xs text-muted-foreground">
|
|
175
|
+
Reporting window
|
|
176
|
+
</dt>
|
|
177
|
+
<dd className="text-sm font-medium">{activityCopy.period}</dd>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex flex-col gap-1 border-t px-5 py-4 sm:border-t-0">
|
|
180
|
+
<dt className="text-xs text-muted-foreground">As of</dt>
|
|
181
|
+
<dd className="text-xs font-medium tabular-nums">
|
|
182
|
+
{asOfFormatter.format(new Date(active.asOfMs))} UTC
|
|
183
|
+
</dd>
|
|
184
|
+
</div>
|
|
185
|
+
</dl>
|
|
186
|
+
</CardFooter>
|
|
187
|
+
</Card>
|
|
188
|
+
</section>
|
|
189
|
+
|
|
190
|
+
<section aria-labelledby="bundle-detail-heading">
|
|
191
|
+
<UpdateOutcomes
|
|
192
|
+
bundleSelector={
|
|
193
|
+
<BundleSelector
|
|
194
|
+
bundleId={bundleId}
|
|
195
|
+
bundles={bundles}
|
|
196
|
+
onBundleChange={onBundleChange}
|
|
197
|
+
/>
|
|
198
|
+
}
|
|
199
|
+
configuredPercentage={configuredPercentage}
|
|
200
|
+
latestBundleInstallations={latestBundleInstallations}
|
|
201
|
+
reportingInstallations={active.activeInstallations}
|
|
202
|
+
state={outcomes}
|
|
203
|
+
window={active.window}
|
|
204
|
+
/>
|
|
205
|
+
</section>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { TriangleAlert } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { BundleIdDisplay } from "@/components/BundleIdDisplay";
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "@/components/ui/card";
|
|
13
|
+
import { Separator } from "@/components/ui/separator";
|
|
14
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
15
|
+
import {
|
|
16
|
+
Table,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCell,
|
|
19
|
+
TableHead,
|
|
20
|
+
TableHeader,
|
|
21
|
+
TableRow,
|
|
22
|
+
} from "@/components/ui/table";
|
|
23
|
+
import type {
|
|
24
|
+
InstallationHistoryResult,
|
|
25
|
+
InstallationHistoryRow,
|
|
26
|
+
InstallationSearchRow,
|
|
27
|
+
} from "@/lib/api";
|
|
28
|
+
|
|
29
|
+
import { InstallationPagination } from "./InstallationPagination";
|
|
30
|
+
|
|
31
|
+
const formatDateTime = (value: string | number | Date | null | undefined) => {
|
|
32
|
+
if (!value) return "—";
|
|
33
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
34
|
+
if (Number.isNaN(date.getTime())) return "—";
|
|
35
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
36
|
+
year: "numeric",
|
|
37
|
+
month: "2-digit",
|
|
38
|
+
day: "2-digit",
|
|
39
|
+
hour: "2-digit",
|
|
40
|
+
minute: "2-digit",
|
|
41
|
+
second: "2-digit",
|
|
42
|
+
hour12: false,
|
|
43
|
+
timeZone: "UTC",
|
|
44
|
+
}).format(date);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getUserLabel = (event: {
|
|
48
|
+
readonly username: string | null;
|
|
49
|
+
readonly userId: string | null;
|
|
50
|
+
}) => event.userId ?? event.username ?? "—";
|
|
51
|
+
|
|
52
|
+
const getLastKnownBundleId = (
|
|
53
|
+
event: InstallationHistoryRow | InstallationSearchRow,
|
|
54
|
+
) =>
|
|
55
|
+
"lastKnownBundleId" in event
|
|
56
|
+
? event.lastKnownBundleId
|
|
57
|
+
: (event.toBundleId ?? event.fromBundleId);
|
|
58
|
+
|
|
59
|
+
export function InstallationHistoryCard({
|
|
60
|
+
error,
|
|
61
|
+
history,
|
|
62
|
+
isLoading,
|
|
63
|
+
limit,
|
|
64
|
+
offset,
|
|
65
|
+
onOffsetChange,
|
|
66
|
+
selectedEvent,
|
|
67
|
+
selectedInstallId,
|
|
68
|
+
}: {
|
|
69
|
+
readonly error: unknown;
|
|
70
|
+
readonly history: InstallationHistoryResult | undefined;
|
|
71
|
+
readonly isLoading: boolean;
|
|
72
|
+
readonly limit: number;
|
|
73
|
+
readonly offset: number;
|
|
74
|
+
readonly onOffsetChange: (offset: number) => void;
|
|
75
|
+
readonly selectedEvent:
|
|
76
|
+
| InstallationHistoryRow
|
|
77
|
+
| InstallationSearchRow
|
|
78
|
+
| undefined;
|
|
79
|
+
readonly selectedInstallId: string;
|
|
80
|
+
}) {
|
|
81
|
+
const lastKnownBundleId = selectedEvent
|
|
82
|
+
? getLastKnownBundleId(selectedEvent)
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Card className="min-h-0 min-w-0">
|
|
87
|
+
<CardHeader className="p-6 pb-5">
|
|
88
|
+
<div className="flex items-start justify-between gap-4">
|
|
89
|
+
<div className="flex min-w-0 flex-col gap-1.5">
|
|
90
|
+
<CardTitle className="text-sm font-medium">
|
|
91
|
+
<h2>
|
|
92
|
+
{selectedInstallId
|
|
93
|
+
? "Installation history"
|
|
94
|
+
: "Select an installation"}
|
|
95
|
+
</h2>
|
|
96
|
+
</CardTitle>
|
|
97
|
+
<CardDescription>
|
|
98
|
+
{selectedInstallId
|
|
99
|
+
? "Recorded bundle changes for this installation."
|
|
100
|
+
: "Choose a match to view its recorded bundle changes."}
|
|
101
|
+
</CardDescription>
|
|
102
|
+
</div>
|
|
103
|
+
{history && history.pagination.total > 0 ? (
|
|
104
|
+
<Badge variant="outline">{history.pagination.total} events</Badge>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent className="min-h-0 p-0">
|
|
109
|
+
{selectedInstallId ? (
|
|
110
|
+
isLoading ? (
|
|
111
|
+
<div className="flex flex-col gap-3 p-5">
|
|
112
|
+
<Skeleton className="h-10 w-full" />
|
|
113
|
+
<Skeleton className="h-10 w-full" />
|
|
114
|
+
<Skeleton className="h-10 w-full" />
|
|
115
|
+
</div>
|
|
116
|
+
) : history && history.data.length > 0 ? (
|
|
117
|
+
<>
|
|
118
|
+
<section
|
|
119
|
+
aria-labelledby="latest-installation-state"
|
|
120
|
+
className="px-6 pb-6"
|
|
121
|
+
>
|
|
122
|
+
<h3
|
|
123
|
+
className="text-xs font-medium"
|
|
124
|
+
id="latest-installation-state"
|
|
125
|
+
>
|
|
126
|
+
Latest reported state
|
|
127
|
+
</h3>
|
|
128
|
+
<dl className="mt-4 grid gap-6 xl:grid-cols-3">
|
|
129
|
+
<div className="min-w-0">
|
|
130
|
+
<dt className="text-xs text-muted-foreground">
|
|
131
|
+
Installation
|
|
132
|
+
</dt>
|
|
133
|
+
<dd className="mt-1 font-mono text-xs">
|
|
134
|
+
<BundleIdDisplay bundleId={selectedInstallId} />
|
|
135
|
+
</dd>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="min-w-0">
|
|
138
|
+
<dt className="text-xs text-muted-foreground">User ID</dt>
|
|
139
|
+
<dd className="mt-1 truncate text-xs font-medium">
|
|
140
|
+
{selectedEvent ? getUserLabel(selectedEvent) : "—"}
|
|
141
|
+
</dd>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="min-w-0">
|
|
144
|
+
<dt className="text-xs text-muted-foreground">
|
|
145
|
+
Last known bundle
|
|
146
|
+
</dt>
|
|
147
|
+
<dd className="mt-1 font-mono text-xs">
|
|
148
|
+
{lastKnownBundleId ? (
|
|
149
|
+
<BundleIdDisplay bundleId={lastKnownBundleId} />
|
|
150
|
+
) : (
|
|
151
|
+
"—"
|
|
152
|
+
)}
|
|
153
|
+
</dd>
|
|
154
|
+
</div>
|
|
155
|
+
</dl>
|
|
156
|
+
</section>
|
|
157
|
+
<Separator />
|
|
158
|
+
<Table className="min-w-3xl table-fixed">
|
|
159
|
+
<TableHeader>
|
|
160
|
+
<TableRow className="hover:bg-transparent [&>th]:h-12 [&>th]:px-5">
|
|
161
|
+
<TableHead className="w-1/5 whitespace-normal">
|
|
162
|
+
Reported (UTC)
|
|
163
|
+
</TableHead>
|
|
164
|
+
<TableHead className="w-1/6 whitespace-normal">
|
|
165
|
+
App version
|
|
166
|
+
</TableHead>
|
|
167
|
+
<TableHead className="w-1/5 whitespace-normal">
|
|
168
|
+
Change
|
|
169
|
+
</TableHead>
|
|
170
|
+
<TableHead className="whitespace-normal">
|
|
171
|
+
Bundle transition
|
|
172
|
+
</TableHead>
|
|
173
|
+
</TableRow>
|
|
174
|
+
</TableHeader>
|
|
175
|
+
<TableBody>
|
|
176
|
+
{history.data.map((event) => (
|
|
177
|
+
<TableRow
|
|
178
|
+
className="[&>td]:px-5 [&>td]:py-4"
|
|
179
|
+
key={event.id}
|
|
180
|
+
>
|
|
181
|
+
<TableCell className="whitespace-normal align-top text-sm text-muted-foreground">
|
|
182
|
+
{formatDateTime(event.receivedAtMs)}
|
|
183
|
+
</TableCell>
|
|
184
|
+
<TableCell className="whitespace-normal align-top font-mono text-xs">
|
|
185
|
+
{event.appVersion}
|
|
186
|
+
</TableCell>
|
|
187
|
+
<TableCell className="whitespace-normal align-top">
|
|
188
|
+
<Badge variant="secondary">
|
|
189
|
+
{event.type === "UPDATE_APPLIED"
|
|
190
|
+
? "Bundle applied"
|
|
191
|
+
: "Recovered"}
|
|
192
|
+
</Badge>
|
|
193
|
+
</TableCell>
|
|
194
|
+
<TableCell className="whitespace-normal align-top">
|
|
195
|
+
<div className="grid grid-cols-[2rem_minmax(0,1fr)] gap-x-3 gap-y-3">
|
|
196
|
+
<span className="text-muted-foreground">From</span>
|
|
197
|
+
{event.fromBundleId ? (
|
|
198
|
+
<BundleIdDisplay bundleId={event.fromBundleId} />
|
|
199
|
+
) : (
|
|
200
|
+
"—"
|
|
201
|
+
)}
|
|
202
|
+
<span className="text-muted-foreground">To</span>
|
|
203
|
+
{event.toBundleId ? (
|
|
204
|
+
<BundleIdDisplay bundleId={event.toBundleId} />
|
|
205
|
+
) : (
|
|
206
|
+
"—"
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</TableCell>
|
|
210
|
+
</TableRow>
|
|
211
|
+
))}
|
|
212
|
+
</TableBody>
|
|
213
|
+
</Table>
|
|
214
|
+
<InstallationPagination
|
|
215
|
+
label="Installation history"
|
|
216
|
+
limit={limit}
|
|
217
|
+
offset={offset}
|
|
218
|
+
pageLength={history.data.length}
|
|
219
|
+
total={history.pagination.total}
|
|
220
|
+
onOffsetChange={onOffsetChange}
|
|
221
|
+
/>
|
|
222
|
+
</>
|
|
223
|
+
) : error ? (
|
|
224
|
+
<div className="p-5 pt-0">
|
|
225
|
+
<Alert variant="destructive">
|
|
226
|
+
<TriangleAlert aria-hidden="true" />
|
|
227
|
+
<AlertTitle>Installation history unavailable</AlertTitle>
|
|
228
|
+
<AlertDescription>
|
|
229
|
+
{error instanceof Error
|
|
230
|
+
? error.message
|
|
231
|
+
: "Failed to load installation history."}
|
|
232
|
+
</AlertDescription>
|
|
233
|
+
</Alert>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
<div className="p-5 pt-0 text-sm text-muted-foreground">
|
|
237
|
+
No history is available for this installation.
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
) : (
|
|
241
|
+
<div className="p-5 pt-0 text-sm text-muted-foreground">
|
|
242
|
+
Choose an installation to inspect its update history.
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</CardContent>
|
|
246
|
+
</Card>
|
|
247
|
+
);
|
|
248
|
+
}
|