@hot-updater/console 0.36.7 → 1.0.0-rc.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/AnalyticsCapabilityContext-BCke57-7.js +1 -0
- package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +42 -0
- package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +1 -0
- package/.output/public/assets/alert-C-1KvDW8.js +1 -0
- package/.output/public/assets/analytics-Bqz0c-BT.js +1 -0
- package/.output/public/assets/analytics-api-CJdrTLLO.js +1 -0
- package/.output/public/assets/api-B_Rh-uzC.js +1 -0
- package/.output/public/assets/api-keys-BurCFhzH.js +1 -0
- package/.output/public/assets/api-keys-api-BEDJdQg3.js +1 -0
- package/.output/public/assets/badge-Bf0fZ-Gx.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-BS60BRr2.js +1 -0
- package/.output/public/assets/dist-DspNKfMj.js +1 -0
- package/.output/public/assets/download-O-yNBc9H.js +1 -0
- package/.output/public/assets/field-Buq148-A.js +1 -0
- package/.output/public/assets/index-kFhd2S78.js +10 -0
- package/.output/public/assets/installations-BrQ-od41.js +2 -0
- package/.output/public/assets/installations-CGlZ3ROz.js +1 -0
- package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +1 -0
- package/.output/public/assets/loader-circle-DCYF-wJl.js +1 -0
- package/.output/public/assets/preload-helper-TKOBYGYD.js +1 -0
- package/.output/public/assets/refresh-cw-n8XYbPPj.js +1 -0
- package/.output/public/assets/routes-CrdokJ80.js +1 -0
- package/.output/public/assets/routes-DcQnVajO.js +2 -0
- package/.output/public/assets/search-DLhbWw3W.js +1 -0
- package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
- package/.output/public/assets/signing-HRexfjsH.js +1 -0
- package/.output/public/assets/styles-eB4EtvFS.css +2 -0
- package/.output/public/assets/table-D8CLieHX.js +1 -0
- package/.output/public/assets/textarea-YJLTVAoq.js +1 -0
- package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
- package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-DBP9TQ7K.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/AnalyticsCapabilityContext-ui4DX8GO.mjs +15 -0
- package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +288 -0
- package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +13 -0
- package/.output/server/_ssr/alert-Dim3MGY5.mjs +86 -0
- package/.output/server/_ssr/analytics-DtMTn8fn.mjs +858 -0
- package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +71 -0
- package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +96 -0
- package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +155 -0
- package/.output/server/_ssr/api-BTSv1tZb.mjs +282 -0
- package/.output/server/_ssr/api-keys-BnM_o2nS.mjs +300 -0
- package/.output/server/_ssr/api-keys-api-CIywsSF5.mjs +60 -0
- package/.output/server/_ssr/api-keys-rpc-BSp1wkIV.mjs +136 -0
- package/.output/server/_ssr/api-rpc-cG5wd_tO.mjs +1468 -0
- package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
- package/.output/server/_ssr/auth-rpc-BrJ6obHJ.mjs +24 -0
- package/.output/server/_ssr/auth.server-DwKoZoQM.mjs +50 -0
- package/.output/server/_ssr/badge-DCxbEeof.mjs +26 -0
- package/.output/server/_ssr/bundle-signing-rpc-COoQhW8E.mjs +16 -0
- package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +51 -0
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
- package/.output/server/_ssr/config.server-DEiLhKmy.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-9P0S0lJ3.mjs} +68 -50
- package/.output/server/_ssr/databaseRows-CwbNwYSl.mjs +166 -0
- package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
- package/.output/server/_ssr/dialog-CQuRMNZ4.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-DRIXKjZG.mjs +74 -0
- package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
- package/.output/server/_ssr/installations-B-azgAvX.mjs +19 -0
- package/.output/server/_ssr/installations-Ctvzp2JH.mjs +535 -0
- package/.output/server/_ssr/router-C-KUBfOi.mjs +513 -0
- package/.output/server/_ssr/routes-BM4ht9jb.mjs +2286 -0
- package/.output/server/_ssr/routes-Z7TwC6nC.mjs +37 -0
- package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +527 -0
- package/.output/server/_ssr/server-BjzmvXnO.mjs +7468 -0
- package/.output/server/_ssr/signing-k8GxyBDF.mjs +202 -0
- package/.output/server/_ssr/ssr.mjs +4 -5740
- package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
- package/.output/server/_ssr/table-v1yCvYo7.mjs +53 -0
- package/.output/server/_ssr/textarea-DYHnskhb.mjs +14 -0
- package/.output/server/_ssr/uuidv7-BbJ-9D_I.mjs +31 -0
- package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +108 -0
- package/.output/server/index.mjs +256 -53
- package/README.md +29 -8
- package/lib/index.d.mts +36 -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/analytics/ActivityChart.tsx +150 -0
- package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +24 -0
- package/src/components/features/analytics/AnalyticsControls.tsx +115 -0
- package/src/components/features/analytics/AnalyticsErrorAlert.tsx +21 -0
- package/src/components/features/analytics/AnalyticsOverview.tsx +208 -0
- package/src/components/features/analytics/BundleDistribution.tsx +133 -0
- package/src/components/features/analytics/BundleSelector.tsx +114 -0
- package/src/components/features/analytics/InstallationHistoryCard.tsx +248 -0
- package/src/components/features/analytics/InstallationMatchesCard.tsx +132 -0
- package/src/components/features/analytics/InstallationPageHeader.tsx +138 -0
- package/src/components/features/analytics/InstallationPagination.tsx +48 -0
- package/src/components/features/analytics/InstallationSearch.tsx +230 -0
- package/src/components/features/analytics/UpdateOutcomes.tsx +147 -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/BundleAnalyticsSummary.tsx +123 -0
- package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -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/releases/ReleaseEditorSheet.tsx +879 -0
- package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
- package/src/components/features/signing/BundleSigningPage.tsx +247 -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 +48 -0
- package/src/lib/analytics-api.ts +145 -0
- package/src/lib/analytics-error.ts +19 -0
- package/src/lib/analytics-input.ts +187 -0
- package/src/lib/analytics-overview.ts +170 -0
- package/src/lib/analytics-rpc.ts +218 -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/server/auth.server.ts +43 -0
- package/src/lib/server/bundle-signing.server.ts +100 -0
- package/src/lib/server/config.server.ts +109 -0
- package/src/lib/server/console-runtime.server.ts +70 -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/analytics.tsx +139 -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/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,114 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import {
|
|
3
|
+
Combobox,
|
|
4
|
+
ComboboxContent,
|
|
5
|
+
ComboboxEmpty,
|
|
6
|
+
ComboboxInput,
|
|
7
|
+
ComboboxItem,
|
|
8
|
+
ComboboxList,
|
|
9
|
+
ComboboxTrigger,
|
|
10
|
+
} from "@/components/ui/combobox";
|
|
11
|
+
import { Field, FieldLabel } from "@/components/ui/field";
|
|
12
|
+
|
|
13
|
+
type BundleOption = {
|
|
14
|
+
readonly bundleId: string;
|
|
15
|
+
readonly description: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function BundleSelector({
|
|
19
|
+
bundleId,
|
|
20
|
+
bundles,
|
|
21
|
+
onBundleChange,
|
|
22
|
+
}: {
|
|
23
|
+
readonly bundleId: string;
|
|
24
|
+
readonly bundles: readonly BundleOption[];
|
|
25
|
+
readonly onBundleChange: (bundleId: string) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const selectedBundle =
|
|
28
|
+
bundles.find((bundle) => bundle.bundleId === bundleId) ?? null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Field
|
|
32
|
+
className="w-full min-w-0 sm:max-w-md"
|
|
33
|
+
data-disabled={!bundles.length}
|
|
34
|
+
>
|
|
35
|
+
<FieldLabel htmlFor="analytics-bundle-selector">
|
|
36
|
+
Bundle to inspect
|
|
37
|
+
</FieldLabel>
|
|
38
|
+
<Combobox
|
|
39
|
+
autoHighlight
|
|
40
|
+
disabled={!bundles.length}
|
|
41
|
+
filter={(bundle, query) => {
|
|
42
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
43
|
+
return (
|
|
44
|
+
bundle.bundleId.toLowerCase().includes(normalizedQuery) ||
|
|
45
|
+
bundle.description.toLowerCase().includes(normalizedQuery)
|
|
46
|
+
);
|
|
47
|
+
}}
|
|
48
|
+
isItemEqualToValue={(bundle, value) =>
|
|
49
|
+
bundle.bundleId === value.bundleId
|
|
50
|
+
}
|
|
51
|
+
items={bundles}
|
|
52
|
+
itemToStringLabel={(bundle) => bundle.bundleId}
|
|
53
|
+
onValueChange={(bundle) => {
|
|
54
|
+
if (bundle) onBundleChange(bundle.bundleId);
|
|
55
|
+
}}
|
|
56
|
+
value={selectedBundle}
|
|
57
|
+
>
|
|
58
|
+
<ComboboxTrigger
|
|
59
|
+
aria-label="Bundle to inspect"
|
|
60
|
+
id="analytics-bundle-selector"
|
|
61
|
+
render={
|
|
62
|
+
<Button
|
|
63
|
+
className="h-auto min-h-12 w-full min-w-0 justify-between px-3 py-2 text-left"
|
|
64
|
+
size="lg"
|
|
65
|
+
variant="outline"
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
{selectedBundle ? (
|
|
70
|
+
<span className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
|
71
|
+
<code className="max-w-full truncate text-xs/4">
|
|
72
|
+
{selectedBundle.bundleId}
|
|
73
|
+
</code>
|
|
74
|
+
<span className="max-w-full truncate text-xs/4 text-muted-foreground">
|
|
75
|
+
{selectedBundle.description}
|
|
76
|
+
</span>
|
|
77
|
+
</span>
|
|
78
|
+
) : (
|
|
79
|
+
<span className="truncate text-muted-foreground">
|
|
80
|
+
No bundles available
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</ComboboxTrigger>
|
|
84
|
+
<ComboboxContent className="min-w-(--anchor-width)">
|
|
85
|
+
<ComboboxInput
|
|
86
|
+
aria-label="Search bundles"
|
|
87
|
+
placeholder="Search by bundle ID or description"
|
|
88
|
+
showClear
|
|
89
|
+
showTrigger={false}
|
|
90
|
+
/>
|
|
91
|
+
<ComboboxEmpty>No bundles found.</ComboboxEmpty>
|
|
92
|
+
<ComboboxList>
|
|
93
|
+
{(bundle) => (
|
|
94
|
+
<ComboboxItem
|
|
95
|
+
className="min-h-12 px-3 py-2"
|
|
96
|
+
key={bundle.bundleId}
|
|
97
|
+
value={bundle}
|
|
98
|
+
>
|
|
99
|
+
<span className="flex min-w-0 flex-col gap-1 pr-5">
|
|
100
|
+
<code className="max-w-72 truncate text-xs/4">
|
|
101
|
+
{bundle.bundleId}
|
|
102
|
+
</code>
|
|
103
|
+
<span className="truncate text-xs/4 text-muted-foreground">
|
|
104
|
+
{bundle.description}
|
|
105
|
+
</span>
|
|
106
|
+
</span>
|
|
107
|
+
</ComboboxItem>
|
|
108
|
+
)}
|
|
109
|
+
</ComboboxList>
|
|
110
|
+
</ComboboxContent>
|
|
111
|
+
</Combobox>
|
|
112
|
+
</Field>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Check, 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 type {
|
|
14
|
+
InstallationSearchResult,
|
|
15
|
+
InstallationSearchRow,
|
|
16
|
+
} from "@/lib/api";
|
|
17
|
+
|
|
18
|
+
import { InstallationPagination } from "./InstallationPagination";
|
|
19
|
+
|
|
20
|
+
const getLastKnownBundleId = (event: InstallationSearchRow) =>
|
|
21
|
+
event.lastKnownBundleId;
|
|
22
|
+
|
|
23
|
+
const getUserLabel = (event: InstallationSearchRow) =>
|
|
24
|
+
event.userId ?? event.username ?? "—";
|
|
25
|
+
|
|
26
|
+
export function InstallationMatchesCard({
|
|
27
|
+
error,
|
|
28
|
+
limit,
|
|
29
|
+
offset,
|
|
30
|
+
onOffsetChange,
|
|
31
|
+
onSelect,
|
|
32
|
+
results,
|
|
33
|
+
selectedInstallId,
|
|
34
|
+
}: {
|
|
35
|
+
readonly error: unknown;
|
|
36
|
+
readonly limit: number;
|
|
37
|
+
readonly offset: number;
|
|
38
|
+
readonly onOffsetChange: (offset: number) => void;
|
|
39
|
+
readonly onSelect: (installId: string) => void;
|
|
40
|
+
readonly results: InstallationSearchResult | undefined;
|
|
41
|
+
readonly selectedInstallId: string;
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<Card className="min-h-0 min-w-0">
|
|
45
|
+
<CardHeader className="p-6 pb-5">
|
|
46
|
+
<div className="flex items-start justify-between gap-4">
|
|
47
|
+
<div className="flex min-w-0 flex-col gap-1.5">
|
|
48
|
+
<CardTitle className="text-sm font-medium">
|
|
49
|
+
<h2>Matching installations</h2>
|
|
50
|
+
</CardTitle>
|
|
51
|
+
<CardDescription>
|
|
52
|
+
Select a row to review its bundle history.
|
|
53
|
+
</CardDescription>
|
|
54
|
+
</div>
|
|
55
|
+
<Badge variant="outline">{results?.pagination.total ?? 0}</Badge>
|
|
56
|
+
</div>
|
|
57
|
+
</CardHeader>
|
|
58
|
+
<CardContent className="min-h-0 p-0">
|
|
59
|
+
{results && results.data.length > 0 ? (
|
|
60
|
+
<ul aria-label="Matching installations" className="divide-y">
|
|
61
|
+
{results.data.map((event) => {
|
|
62
|
+
const currentBundleId = getLastKnownBundleId(event);
|
|
63
|
+
const isSelected = event.installId === selectedInstallId;
|
|
64
|
+
return (
|
|
65
|
+
<li key={event.installId}>
|
|
66
|
+
<button
|
|
67
|
+
aria-pressed={isSelected}
|
|
68
|
+
className="group flex w-full min-w-0 flex-col gap-4 border-l-2 border-transparent px-6 py-5 text-left outline-none transition-colors hover:bg-muted/40 focus-visible:border-ring focus-visible:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/30 aria-pressed:border-primary aria-pressed:bg-muted/60"
|
|
69
|
+
onClick={() => onSelect(event.installId)}
|
|
70
|
+
type="button"
|
|
71
|
+
>
|
|
72
|
+
<span className="flex w-full min-w-0 items-center justify-between gap-3">
|
|
73
|
+
<span className="min-w-0 truncate text-sm font-medium">
|
|
74
|
+
{getUserLabel(event)}
|
|
75
|
+
</span>
|
|
76
|
+
{isSelected ? (
|
|
77
|
+
<span className="flex shrink-0 items-center gap-1 text-xs font-medium text-primary">
|
|
78
|
+
<Check aria-hidden="true" className="size-3.5" />
|
|
79
|
+
Selected
|
|
80
|
+
</span>
|
|
81
|
+
) : null}
|
|
82
|
+
</span>
|
|
83
|
+
<BundleIdDisplay
|
|
84
|
+
bundleId={event.installId}
|
|
85
|
+
className="block text-muted-foreground"
|
|
86
|
+
/>
|
|
87
|
+
<span className="min-w-0">
|
|
88
|
+
<span className="mb-1 block text-xs text-muted-foreground">
|
|
89
|
+
Last known bundle
|
|
90
|
+
</span>
|
|
91
|
+
{currentBundleId ? (
|
|
92
|
+
<BundleIdDisplay bundleId={currentBundleId} />
|
|
93
|
+
) : (
|
|
94
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
95
|
+
)}
|
|
96
|
+
</span>
|
|
97
|
+
</button>
|
|
98
|
+
</li>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</ul>
|
|
102
|
+
) : error ? (
|
|
103
|
+
<div className="p-5">
|
|
104
|
+
<Alert variant="destructive">
|
|
105
|
+
<TriangleAlert aria-hidden="true" />
|
|
106
|
+
<AlertTitle>Installation search unavailable</AlertTitle>
|
|
107
|
+
<AlertDescription>
|
|
108
|
+
{error instanceof Error
|
|
109
|
+
? error.message
|
|
110
|
+
: "Failed to load installations."}
|
|
111
|
+
</AlertDescription>
|
|
112
|
+
</Alert>
|
|
113
|
+
</div>
|
|
114
|
+
) : (
|
|
115
|
+
<div className="p-5 text-sm text-muted-foreground">
|
|
116
|
+
No installations matched that query.
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
{results && results.pagination.total > 0 ? (
|
|
120
|
+
<InstallationPagination
|
|
121
|
+
label="Installation results"
|
|
122
|
+
limit={limit}
|
|
123
|
+
offset={offset}
|
|
124
|
+
pageLength={results.data.length}
|
|
125
|
+
total={results.pagination.total}
|
|
126
|
+
onOffsetChange={onOffsetChange}
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { ArrowLeft, Search, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Button, buttonVariants } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/ui/card";
|
|
11
|
+
import {
|
|
12
|
+
Field,
|
|
13
|
+
FieldDescription,
|
|
14
|
+
FieldGroup,
|
|
15
|
+
FieldLabel,
|
|
16
|
+
} from "@/components/ui/field";
|
|
17
|
+
import {
|
|
18
|
+
InputGroup,
|
|
19
|
+
InputGroupAddon,
|
|
20
|
+
InputGroupButton,
|
|
21
|
+
InputGroupInput,
|
|
22
|
+
} from "@/components/ui/input-group";
|
|
23
|
+
import { Separator } from "@/components/ui/separator";
|
|
24
|
+
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
25
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
26
|
+
|
|
27
|
+
export function InstallationPageHeader() {
|
|
28
|
+
return (
|
|
29
|
+
<header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-x-2 gap-y-1 border-b bg-background px-3 py-3 sm:min-h-12 sm:flex-nowrap sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
|
|
30
|
+
<SidebarTrigger className="-ml-1" />
|
|
31
|
+
<Link
|
|
32
|
+
className={buttonVariants({ size: "sm", variant: "ghost" })}
|
|
33
|
+
to="/analytics"
|
|
34
|
+
>
|
|
35
|
+
<ArrowLeft aria-hidden="true" data-icon="inline-start" />
|
|
36
|
+
Back to Analytics
|
|
37
|
+
</Link>
|
|
38
|
+
<Separator className="mx-1 hidden h-4 sm:block" orientation="vertical" />
|
|
39
|
+
<div className="basis-full pl-9 sm:basis-auto sm:pl-0">
|
|
40
|
+
<h1 className="text-sm font-medium">Installation history</h1>
|
|
41
|
+
<p className="text-xs text-muted-foreground">
|
|
42
|
+
Review the last known bundle and recorded changes.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function InstallationSearchPanel({
|
|
50
|
+
draftQuery,
|
|
51
|
+
onDraftQueryChange,
|
|
52
|
+
onSubmit,
|
|
53
|
+
onClear,
|
|
54
|
+
hasQuery,
|
|
55
|
+
}: {
|
|
56
|
+
readonly draftQuery: string;
|
|
57
|
+
readonly onDraftQueryChange: (value: string) => void;
|
|
58
|
+
readonly onSubmit: () => void;
|
|
59
|
+
readonly onClear: () => void;
|
|
60
|
+
readonly hasQuery: boolean;
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardContent className="grid gap-4 p-4 lg:grid-cols-[minmax(12rem,0.45fr)_minmax(0,1.55fr)] lg:items-start">
|
|
65
|
+
<div className="flex min-w-0 flex-col gap-1.5">
|
|
66
|
+
<CardTitle className="text-sm font-medium">
|
|
67
|
+
<h2>Find an installation</h2>
|
|
68
|
+
</CardTitle>
|
|
69
|
+
<CardDescription>
|
|
70
|
+
Search by either identifier to inspect its bundle history.
|
|
71
|
+
</CardDescription>
|
|
72
|
+
</div>
|
|
73
|
+
<form
|
|
74
|
+
onSubmit={(event) => {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
onSubmit();
|
|
77
|
+
}}
|
|
78
|
+
role="search"
|
|
79
|
+
>
|
|
80
|
+
<FieldGroup>
|
|
81
|
+
<Field>
|
|
82
|
+
<FieldLabel htmlFor="installation-history-search">
|
|
83
|
+
User ID or install ID
|
|
84
|
+
</FieldLabel>
|
|
85
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
86
|
+
<InputGroup className="h-8">
|
|
87
|
+
<InputGroupAddon>
|
|
88
|
+
<Search aria-hidden="true" />
|
|
89
|
+
</InputGroupAddon>
|
|
90
|
+
<InputGroupInput
|
|
91
|
+
id="installation-history-search"
|
|
92
|
+
onChange={(event) => onDraftQueryChange(event.target.value)}
|
|
93
|
+
placeholder="Enter a user ID or install ID"
|
|
94
|
+
type="search"
|
|
95
|
+
value={draftQuery}
|
|
96
|
+
/>
|
|
97
|
+
{draftQuery.length > 0 || hasQuery ? (
|
|
98
|
+
<InputGroupAddon align="inline-end">
|
|
99
|
+
<InputGroupButton
|
|
100
|
+
aria-label="Clear search"
|
|
101
|
+
onClick={onClear}
|
|
102
|
+
size="icon-xs"
|
|
103
|
+
>
|
|
104
|
+
<X aria-hidden="true" />
|
|
105
|
+
</InputGroupButton>
|
|
106
|
+
</InputGroupAddon>
|
|
107
|
+
) : null}
|
|
108
|
+
</InputGroup>
|
|
109
|
+
<Button
|
|
110
|
+
className="w-full sm:w-auto"
|
|
111
|
+
disabled={draftQuery.trim().length === 0}
|
|
112
|
+
size="lg"
|
|
113
|
+
type="submit"
|
|
114
|
+
>
|
|
115
|
+
<Search aria-hidden="true" data-icon="inline-start" />
|
|
116
|
+
Search history
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
<FieldDescription>
|
|
120
|
+
A user ID may return more than one installation. An install ID
|
|
121
|
+
opens one installation directly.
|
|
122
|
+
</FieldDescription>
|
|
123
|
+
</Field>
|
|
124
|
+
</FieldGroup>
|
|
125
|
+
</form>
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function InstallationResultsSkeleton() {
|
|
132
|
+
return (
|
|
133
|
+
<div className="grid items-stretch gap-6 lg:min-h-96 lg:grid-cols-[minmax(18rem,20rem)_minmax(0,1fr)]">
|
|
134
|
+
<Skeleton className="h-64 w-full lg:h-full" />
|
|
135
|
+
<Skeleton className="h-64 w-full lg:h-full" />
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
|
|
3
|
+
export function InstallationPagination({
|
|
4
|
+
label,
|
|
5
|
+
limit,
|
|
6
|
+
offset,
|
|
7
|
+
pageLength,
|
|
8
|
+
total,
|
|
9
|
+
onOffsetChange,
|
|
10
|
+
}: {
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly limit: number;
|
|
13
|
+
readonly offset: number;
|
|
14
|
+
readonly pageLength: number;
|
|
15
|
+
readonly total: number;
|
|
16
|
+
readonly onOffsetChange: (offset: number) => void;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<nav
|
|
20
|
+
aria-label={`${label} pagination`}
|
|
21
|
+
className="flex items-center justify-between gap-4 border-t px-5 py-5"
|
|
22
|
+
>
|
|
23
|
+
<span className="text-xs text-muted-foreground">
|
|
24
|
+
{offset + 1}–{Math.min(offset + pageLength, total)} of {total}
|
|
25
|
+
</span>
|
|
26
|
+
<div className="flex gap-2">
|
|
27
|
+
<Button
|
|
28
|
+
disabled={offset === 0}
|
|
29
|
+
onClick={() => onOffsetChange(Math.max(0, offset - limit))}
|
|
30
|
+
size="sm"
|
|
31
|
+
type="button"
|
|
32
|
+
variant="outline"
|
|
33
|
+
>
|
|
34
|
+
Previous
|
|
35
|
+
</Button>
|
|
36
|
+
<Button
|
|
37
|
+
disabled={offset + pageLength >= total}
|
|
38
|
+
onClick={() => onOffsetChange(offset + limit)}
|
|
39
|
+
size="sm"
|
|
40
|
+
type="button"
|
|
41
|
+
variant="outline"
|
|
42
|
+
>
|
|
43
|
+
Next
|
|
44
|
+
</Button>
|
|
45
|
+
</div>
|
|
46
|
+
</nav>
|
|
47
|
+
);
|
|
48
|
+
}
|