@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,228 @@
|
|
|
1
|
+
import { createUUIDv7, type ChannelRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import { Loader2, Plus, Tag, Trash2 } from "lucide-react";
|
|
3
|
+
import { type FormEvent, useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
AlertDialog,
|
|
8
|
+
AlertDialogAction,
|
|
9
|
+
AlertDialogCancel,
|
|
10
|
+
AlertDialogContent,
|
|
11
|
+
AlertDialogDescription,
|
|
12
|
+
AlertDialogFooter,
|
|
13
|
+
AlertDialogHeader,
|
|
14
|
+
AlertDialogTitle,
|
|
15
|
+
} from "@/components/ui/alert-dialog";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import {
|
|
18
|
+
Dialog,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogDescription,
|
|
21
|
+
DialogHeader,
|
|
22
|
+
DialogTitle,
|
|
23
|
+
} from "@/components/ui/dialog";
|
|
24
|
+
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import {
|
|
27
|
+
useChannelsQuery,
|
|
28
|
+
useCreateChannelMutation,
|
|
29
|
+
useDeleteChannelMutation,
|
|
30
|
+
} from "@/lib/api";
|
|
31
|
+
|
|
32
|
+
interface ChannelManagementDialogProps {
|
|
33
|
+
open: boolean;
|
|
34
|
+
onOpenChange: (open: boolean) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ChannelManagementDialog({
|
|
38
|
+
open,
|
|
39
|
+
onOpenChange,
|
|
40
|
+
}: ChannelManagementDialogProps) {
|
|
41
|
+
const [channelName, setChannelName] = useState("");
|
|
42
|
+
const [channelToDelete, setChannelToDelete] = useState<ChannelRow | null>(
|
|
43
|
+
null,
|
|
44
|
+
);
|
|
45
|
+
const { data: channels = [], isPending: isLoadingChannels } =
|
|
46
|
+
useChannelsQuery();
|
|
47
|
+
const createChannel = useCreateChannelMutation();
|
|
48
|
+
const deleteChannel = useDeleteChannelMutation();
|
|
49
|
+
const normalizedName = channelName.trim();
|
|
50
|
+
|
|
51
|
+
const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
if (!normalizedName || createChannel.isPending) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await createChannel.mutateAsync({
|
|
59
|
+
row: { id: createUUIDv7(), name: normalizedName },
|
|
60
|
+
onConflict: "returnExisting",
|
|
61
|
+
});
|
|
62
|
+
setChannelName("");
|
|
63
|
+
if (result.inserted) {
|
|
64
|
+
toast.success(`Channel ${result.row.name} created`);
|
|
65
|
+
} else {
|
|
66
|
+
toast.info(`Channel ${result.row.name} already exists`);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
toast.error("Failed to create channel");
|
|
70
|
+
console.error(error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleDelete = async () => {
|
|
75
|
+
if (!channelToDelete || deleteChannel.isPending) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await deleteChannel.mutateAsync({
|
|
81
|
+
id: channelToDelete.id,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.deleted) {
|
|
85
|
+
toast.success(`Channel ${channelToDelete.name} deleted`);
|
|
86
|
+
} else if (result.reason === "not_empty") {
|
|
87
|
+
toast.error(
|
|
88
|
+
`Channel ${channelToDelete.name} is still in use and cannot be deleted`,
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
toast.error(`Channel ${channelToDelete.name} no longer exists`);
|
|
92
|
+
}
|
|
93
|
+
setChannelToDelete(null);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
toast.error("Failed to delete channel");
|
|
96
|
+
console.error(error);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
103
|
+
<DialogContent className="sm:max-w-lg">
|
|
104
|
+
<DialogHeader>
|
|
105
|
+
<DialogTitle className="flex items-center gap-2">
|
|
106
|
+
<Tag className="size-4 text-muted-foreground" />
|
|
107
|
+
Channels
|
|
108
|
+
</DialogTitle>
|
|
109
|
+
<DialogDescription>
|
|
110
|
+
Create deployment channels and remove them when no Release uses
|
|
111
|
+
them. Empty channels remain available until you delete them.
|
|
112
|
+
</DialogDescription>
|
|
113
|
+
</DialogHeader>
|
|
114
|
+
|
|
115
|
+
<form onSubmit={handleCreate}>
|
|
116
|
+
<FieldGroup className="gap-2">
|
|
117
|
+
<Field orientation="horizontal">
|
|
118
|
+
<FieldLabel htmlFor="channel-name" className="sr-only">
|
|
119
|
+
Channel name
|
|
120
|
+
</FieldLabel>
|
|
121
|
+
<Input
|
|
122
|
+
id="channel-name"
|
|
123
|
+
value={channelName}
|
|
124
|
+
onChange={(event) => setChannelName(event.target.value)}
|
|
125
|
+
placeholder="Channel name"
|
|
126
|
+
autoComplete="off"
|
|
127
|
+
disabled={createChannel.isPending}
|
|
128
|
+
/>
|
|
129
|
+
<Button
|
|
130
|
+
type="submit"
|
|
131
|
+
disabled={!normalizedName || createChannel.isPending}
|
|
132
|
+
>
|
|
133
|
+
{createChannel.isPending ? (
|
|
134
|
+
<Loader2
|
|
135
|
+
data-icon="inline-start"
|
|
136
|
+
className="animate-spin"
|
|
137
|
+
/>
|
|
138
|
+
) : (
|
|
139
|
+
<Plus data-icon="inline-start" />
|
|
140
|
+
)}
|
|
141
|
+
Create
|
|
142
|
+
</Button>
|
|
143
|
+
</Field>
|
|
144
|
+
</FieldGroup>
|
|
145
|
+
</form>
|
|
146
|
+
|
|
147
|
+
<div className="max-h-72 overflow-y-auto rounded-lg border">
|
|
148
|
+
{isLoadingChannels ? (
|
|
149
|
+
<div className="flex h-20 items-center justify-center text-muted-foreground">
|
|
150
|
+
<Loader2 className="mr-2 size-3.5 animate-spin" />
|
|
151
|
+
Loading channels
|
|
152
|
+
</div>
|
|
153
|
+
) : channels.length === 0 ? (
|
|
154
|
+
<div className="flex h-20 items-center justify-center text-muted-foreground">
|
|
155
|
+
No channels yet
|
|
156
|
+
</div>
|
|
157
|
+
) : (
|
|
158
|
+
<ul className="divide-y">
|
|
159
|
+
{channels.map((channel) => (
|
|
160
|
+
<li
|
|
161
|
+
key={channel.id}
|
|
162
|
+
className="flex min-h-12 items-center gap-3 px-3 py-2"
|
|
163
|
+
>
|
|
164
|
+
<div className="min-w-0 flex-1">
|
|
165
|
+
<p className="truncate text-xs font-medium">
|
|
166
|
+
{channel.name}
|
|
167
|
+
</p>
|
|
168
|
+
<p className="truncate font-mono text-[10px] text-muted-foreground">
|
|
169
|
+
{channel.id}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
<Button
|
|
173
|
+
type="button"
|
|
174
|
+
variant="ghost"
|
|
175
|
+
size="icon-sm"
|
|
176
|
+
aria-label={`Delete ${channel.name}`}
|
|
177
|
+
title={`Delete ${channel.name}`}
|
|
178
|
+
disabled={deleteChannel.isPending}
|
|
179
|
+
onClick={() => setChannelToDelete(channel)}
|
|
180
|
+
className="text-muted-foreground hover:text-destructive"
|
|
181
|
+
>
|
|
182
|
+
<Trash2 data-icon="inline-start" />
|
|
183
|
+
</Button>
|
|
184
|
+
</li>
|
|
185
|
+
))}
|
|
186
|
+
</ul>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</DialogContent>
|
|
190
|
+
</Dialog>
|
|
191
|
+
|
|
192
|
+
<AlertDialog
|
|
193
|
+
open={channelToDelete !== null}
|
|
194
|
+
onOpenChange={(nextOpen) => {
|
|
195
|
+
if (!nextOpen && !deleteChannel.isPending) {
|
|
196
|
+
setChannelToDelete(null);
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<AlertDialogContent>
|
|
201
|
+
<AlertDialogHeader>
|
|
202
|
+
<AlertDialogTitle>Delete {channelToDelete?.name}?</AlertDialogTitle>
|
|
203
|
+
<AlertDialogDescription>
|
|
204
|
+
Only empty channels can be removed. The server checks this when
|
|
205
|
+
you delete it, and the channel can be created again later with a
|
|
206
|
+
new ID.
|
|
207
|
+
</AlertDialogDescription>
|
|
208
|
+
</AlertDialogHeader>
|
|
209
|
+
<AlertDialogFooter>
|
|
210
|
+
<AlertDialogCancel disabled={deleteChannel.isPending}>
|
|
211
|
+
Cancel
|
|
212
|
+
</AlertDialogCancel>
|
|
213
|
+
<AlertDialogAction
|
|
214
|
+
variant="destructive"
|
|
215
|
+
disabled={deleteChannel.isPending}
|
|
216
|
+
onClick={(event) => {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
void handleDelete();
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{deleteChannel.isPending ? "Deleting..." : "Delete channel"}
|
|
222
|
+
</AlertDialogAction>
|
|
223
|
+
</AlertDialogFooter>
|
|
224
|
+
</AlertDialogContent>
|
|
225
|
+
</AlertDialog>
|
|
226
|
+
</>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActiveInstallationOverview,
|
|
3
|
+
ActiveInstallationWindow,
|
|
4
|
+
} from "@hot-updater/server";
|
|
5
|
+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ChartConfig,
|
|
9
|
+
ChartContainer,
|
|
10
|
+
ChartTooltip,
|
|
11
|
+
ChartTooltipContent,
|
|
12
|
+
} from "@/components/ui/chart";
|
|
13
|
+
|
|
14
|
+
type ActivitySeries = ActiveInstallationOverview["series"];
|
|
15
|
+
|
|
16
|
+
const dayFormatter = new Intl.DateTimeFormat("en", {
|
|
17
|
+
day: "numeric",
|
|
18
|
+
month: "short",
|
|
19
|
+
timeZone: "UTC",
|
|
20
|
+
});
|
|
21
|
+
const hourFormatter = new Intl.DateTimeFormat("en", {
|
|
22
|
+
day: "numeric",
|
|
23
|
+
hour: "numeric",
|
|
24
|
+
month: "short",
|
|
25
|
+
timeZone: "UTC",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const formatBucket = (
|
|
29
|
+
bucketStartMs: number,
|
|
30
|
+
window: ActiveInstallationWindow,
|
|
31
|
+
): string =>
|
|
32
|
+
(window === "24h" ? hourFormatter : dayFormatter).format(
|
|
33
|
+
new Date(bucketStartMs),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const chartConfig = {
|
|
37
|
+
installations: {
|
|
38
|
+
label: "Active installations",
|
|
39
|
+
color: "var(--chart-2)",
|
|
40
|
+
},
|
|
41
|
+
} satisfies ChartConfig;
|
|
42
|
+
|
|
43
|
+
export function ActivityChart({
|
|
44
|
+
series,
|
|
45
|
+
window,
|
|
46
|
+
}: {
|
|
47
|
+
readonly series: ActivitySeries;
|
|
48
|
+
readonly window: ActiveInstallationWindow;
|
|
49
|
+
}) {
|
|
50
|
+
const chartData = series.map((point) => ({
|
|
51
|
+
bucketStartMs: point.bucketStartMs,
|
|
52
|
+
installations: point.value,
|
|
53
|
+
}));
|
|
54
|
+
const hasReports = series.some(({ value }) => value > 0);
|
|
55
|
+
const bucketName = window === "24h" ? "hour" : "day";
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex min-w-0 flex-col gap-3">
|
|
59
|
+
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
|
60
|
+
<div>
|
|
61
|
+
<h3 className="text-sm font-medium">Activity over time</h3>
|
|
62
|
+
<p className="text-xs text-muted-foreground">
|
|
63
|
+
Unique active installations per {bucketName}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
<span className="text-xs text-muted-foreground">UTC</span>
|
|
67
|
+
</div>
|
|
68
|
+
{!hasReports && (
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
No installations reported during this period.
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
<ChartContainer
|
|
74
|
+
aria-label={`Active installations per ${bucketName}`}
|
|
75
|
+
className="h-64 w-full aspect-auto"
|
|
76
|
+
config={chartConfig}
|
|
77
|
+
role="img"
|
|
78
|
+
>
|
|
79
|
+
<AreaChart
|
|
80
|
+
accessibilityLayer
|
|
81
|
+
data={chartData}
|
|
82
|
+
margin={{ left: -16, right: 4, top: 8 }}
|
|
83
|
+
>
|
|
84
|
+
<CartesianGrid vertical={false} />
|
|
85
|
+
<XAxis
|
|
86
|
+
axisLine={false}
|
|
87
|
+
dataKey="bucketStartMs"
|
|
88
|
+
minTickGap={28}
|
|
89
|
+
tickFormatter={(value: number) => formatBucket(value, window)}
|
|
90
|
+
tickLine={false}
|
|
91
|
+
/>
|
|
92
|
+
<YAxis
|
|
93
|
+
allowDecimals={false}
|
|
94
|
+
axisLine={false}
|
|
95
|
+
tickLine={false}
|
|
96
|
+
width={32}
|
|
97
|
+
/>
|
|
98
|
+
<ChartTooltip
|
|
99
|
+
content={
|
|
100
|
+
<ChartTooltipContent
|
|
101
|
+
labelFormatter={(_, payload) => {
|
|
102
|
+
const bucket = payload[0]?.payload.bucketStartMs;
|
|
103
|
+
return typeof bucket === "number"
|
|
104
|
+
? formatBucket(bucket, window)
|
|
105
|
+
: "";
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
<Area
|
|
111
|
+
dataKey="installations"
|
|
112
|
+
fill="var(--color-installations)"
|
|
113
|
+
fillOpacity={0.12}
|
|
114
|
+
isAnimationActive={false}
|
|
115
|
+
stroke="var(--color-installations)"
|
|
116
|
+
strokeWidth={2}
|
|
117
|
+
type="monotone"
|
|
118
|
+
/>
|
|
119
|
+
</AreaChart>
|
|
120
|
+
</ChartContainer>
|
|
121
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
122
|
+
Each point counts an installation once in that {bucketName}. The total
|
|
123
|
+
above counts it once across the whole period, so the points do not add
|
|
124
|
+
up to the total.
|
|
125
|
+
</p>
|
|
126
|
+
<div className="sr-only">
|
|
127
|
+
<table aria-label={`Exact active installations per ${bucketName}`}>
|
|
128
|
+
<caption>
|
|
129
|
+
Unique installations that reported an update status in each UTC
|
|
130
|
+
{` ${bucketName}`}.
|
|
131
|
+
</caption>
|
|
132
|
+
<thead>
|
|
133
|
+
<tr>
|
|
134
|
+
<th scope="col">UTC {bucketName}</th>
|
|
135
|
+
<th scope="col">Active installations</th>
|
|
136
|
+
</tr>
|
|
137
|
+
</thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
{chartData.map((point) => (
|
|
140
|
+
<tr key={point.bucketStartMs}>
|
|
141
|
+
<th scope="row">{formatBucket(point.bucketStartMs, window)}</th>
|
|
142
|
+
<td>{point.installations}</td>
|
|
143
|
+
</tr>
|
|
144
|
+
))}
|
|
145
|
+
</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ActiveInstallationOverview } from "@hot-updater/server";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Progress } from "@/components/ui/progress";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@/components/ui/table";
|
|
14
|
+
import type { InsightsOverview } from "@/lib/insights-overview";
|
|
15
|
+
|
|
16
|
+
const PAGE_SIZE = 8;
|
|
17
|
+
|
|
18
|
+
const percentage = (value: number): string =>
|
|
19
|
+
new Intl.NumberFormat(undefined, {
|
|
20
|
+
maximumFractionDigits: 1,
|
|
21
|
+
style: "percent",
|
|
22
|
+
}).format(value);
|
|
23
|
+
|
|
24
|
+
export function BundleDistribution({
|
|
25
|
+
active,
|
|
26
|
+
catalog,
|
|
27
|
+
}: {
|
|
28
|
+
readonly active: ActiveInstallationOverview;
|
|
29
|
+
readonly catalog: InsightsOverview;
|
|
30
|
+
}) {
|
|
31
|
+
const [page, setPage] = useState(1);
|
|
32
|
+
const metadata = new Map(
|
|
33
|
+
catalog.configuredRollouts.map(({ bundle, bundleId }) => [
|
|
34
|
+
bundleId,
|
|
35
|
+
bundle,
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (active.bundles.length === 0) {
|
|
40
|
+
return (
|
|
41
|
+
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
42
|
+
No observed installations in this range.
|
|
43
|
+
</p>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const totalPages = Math.ceil(active.bundles.length / PAGE_SIZE);
|
|
48
|
+
const currentPage = Math.min(page, totalPages);
|
|
49
|
+
const start = (currentPage - 1) * PAGE_SIZE;
|
|
50
|
+
const end = Math.min(start + PAGE_SIZE, active.bundles.length);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex min-w-0 flex-col gap-4">
|
|
54
|
+
<div className="overflow-x-auto">
|
|
55
|
+
<Table aria-label="Observed installations by bundle">
|
|
56
|
+
<TableHeader>
|
|
57
|
+
<TableRow className="hover:bg-transparent">
|
|
58
|
+
<TableHead>Bundle</TableHead>
|
|
59
|
+
<TableHead className="hidden w-2/5 sm:table-cell">
|
|
60
|
+
Activity
|
|
61
|
+
</TableHead>
|
|
62
|
+
<TableHead className="text-right">Observed</TableHead>
|
|
63
|
+
<TableHead className="text-right">Share</TableHead>
|
|
64
|
+
</TableRow>
|
|
65
|
+
</TableHeader>
|
|
66
|
+
<TableBody>
|
|
67
|
+
{active.bundles.slice(start, end).map((row) => {
|
|
68
|
+
const bundle = metadata.get(row.bundleId);
|
|
69
|
+
const share = row.installations / active.activeInstallations;
|
|
70
|
+
const shareLabel = percentage(share);
|
|
71
|
+
return (
|
|
72
|
+
<TableRow key={row.bundleId}>
|
|
73
|
+
<TableCell className="max-w-48 whitespace-normal">
|
|
74
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
75
|
+
<code className="break-all text-xs">{row.bundleId}</code>
|
|
76
|
+
<span className="text-muted-foreground">
|
|
77
|
+
{bundle
|
|
78
|
+
? `${bundle.platform} · ${bundle.channel}`
|
|
79
|
+
: "Unknown bundle metadata"}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</TableCell>
|
|
83
|
+
<TableCell className="hidden sm:table-cell">
|
|
84
|
+
<Progress
|
|
85
|
+
aria-label={`${row.bundleId} observed share ${shareLabel}`}
|
|
86
|
+
aria-valuetext={shareLabel}
|
|
87
|
+
className="h-2"
|
|
88
|
+
value={share * 100}
|
|
89
|
+
/>
|
|
90
|
+
</TableCell>
|
|
91
|
+
<TableCell className="text-right tabular-nums">
|
|
92
|
+
{row.installations.toLocaleString()}
|
|
93
|
+
</TableCell>
|
|
94
|
+
<TableCell className="text-right tabular-nums">
|
|
95
|
+
{shareLabel}
|
|
96
|
+
</TableCell>
|
|
97
|
+
</TableRow>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</TableBody>
|
|
101
|
+
</Table>
|
|
102
|
+
</div>
|
|
103
|
+
{totalPages > 1 && (
|
|
104
|
+
<div className="flex flex-wrap items-center justify-between gap-2 px-4 pb-4">
|
|
105
|
+
<p className="text-xs text-muted-foreground tabular-nums">
|
|
106
|
+
Showing {start + 1}–{end} of{" "}
|
|
107
|
+
{active.bundles.length.toLocaleString()}
|
|
108
|
+
</p>
|
|
109
|
+
<div className="flex gap-2">
|
|
110
|
+
<Button
|
|
111
|
+
disabled={currentPage === 1}
|
|
112
|
+
onClick={() => setPage(currentPage - 1)}
|
|
113
|
+
size="sm"
|
|
114
|
+
type="button"
|
|
115
|
+
variant="outline"
|
|
116
|
+
>
|
|
117
|
+
Previous
|
|
118
|
+
</Button>
|
|
119
|
+
<Button
|
|
120
|
+
disabled={currentPage === totalPages}
|
|
121
|
+
onClick={() => setPage(currentPage + 1)}
|
|
122
|
+
size="sm"
|
|
123
|
+
type="button"
|
|
124
|
+
variant="outline"
|
|
125
|
+
>
|
|
126
|
+
Next
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -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="insights-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="insights-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,24 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { InsightsCapabilityState } from "@/lib/insights-api";
|
|
4
|
+
|
|
5
|
+
const InsightsCapabilityContext = createContext<InsightsCapabilityState>({
|
|
6
|
+
status: "unresolved",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function InsightsCapabilityProvider({
|
|
10
|
+
children,
|
|
11
|
+
value,
|
|
12
|
+
}: {
|
|
13
|
+
readonly children: ReactNode;
|
|
14
|
+
readonly value: InsightsCapabilityState;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<InsightsCapabilityContext.Provider value={value}>
|
|
18
|
+
{children}
|
|
19
|
+
</InsightsCapabilityContext.Provider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useInsightsCapability = (): InsightsCapabilityState =>
|
|
24
|
+
useContext(InsightsCapabilityContext);
|